@redacto.io/consent-sdk-react 3.4.0 → 3.6.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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +176 -0
- package/dist/index.d.mts +51 -4
- package/dist/index.d.ts +51 -4
- package/dist/index.js +1912 -345
- package/dist/index.mjs +1911 -345
- package/dist/privacy-center.d.mts +566 -0
- package/dist/privacy-center.d.ts +566 -0
- package/dist/privacy-center.js +17174 -0
- package/dist/privacy-center.mjs +17136 -0
- package/package.json +47 -3
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.test.tsx +201 -118
- package/src/RedactoNoticeConsent/RedactoNoticeConsent.tsx +1194 -93
- package/src/RedactoNoticeConsent/api/index.ts +337 -246
- package/src/RedactoNoticeConsent/api/types.ts +140 -41
- package/src/RedactoNoticeConsent/components/DigiLockerCallback.tsx +602 -0
- package/src/RedactoNoticeConsent/components/index.ts +7 -0
- package/src/RedactoNoticeConsent/injectStyles.ts +11 -0
- package/src/RedactoNoticeConsent/language-codes.ts +61 -0
- package/src/RedactoNoticeConsent/services/storage.ts +122 -0
- package/src/RedactoNoticeConsent/types.ts +2 -0
- package/src/RedactoNoticeConsentInline/api/index.ts +26 -64
- package/src/RedactoNoticeConsentInline/api/types.ts +0 -25
- package/src/RedactoPrivacyCenter/PrivacyCenterActivityPage.tsx +19 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterConsentManagerPage.tsx +17 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterFormPage.tsx +79 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterHomePage.tsx +27 -0
- package/src/RedactoPrivacyCenter/PrivacyCenterLayout.tsx +142 -0
- package/src/RedactoPrivacyCenter/RedactoPrivacyCenter.tsx +51 -0
- package/src/RedactoPrivacyCenter/api/actions.ts +444 -0
- package/src/RedactoPrivacyCenter/api/client.ts +413 -0
- package/src/RedactoPrivacyCenter/api/config.ts +33 -0
- package/src/RedactoPrivacyCenter/api/fetcher.ts +181 -0
- package/src/RedactoPrivacyCenter/api/upload.ts +93 -0
- package/src/RedactoPrivacyCenter/assets/character.svg +31 -0
- package/src/RedactoPrivacyCenter/assets/fileAttach.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/greenTick.svg +4 -0
- package/src/RedactoPrivacyCenter/assets/mail.svg +3 -0
- package/src/RedactoPrivacyCenter/assets/poweredByRedacto.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/sentImg.svg +9 -0
- package/src/RedactoPrivacyCenter/assets/uploadFileIcon.svg +9 -0
- package/src/RedactoPrivacyCenter/components/ActivitiesList.tsx +272 -0
- package/src/RedactoPrivacyCenter/components/CaseDetailsModal.tsx +468 -0
- package/src/RedactoPrivacyCenter/components/CaseHistory.tsx +433 -0
- package/src/RedactoPrivacyCenter/components/ConsentManager.tsx +726 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerColumns.tsx +239 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerConstants.ts +7 -0
- package/src/RedactoPrivacyCenter/components/ConsentManagerTableSkeleton.tsx +155 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/CorrectionDataSection.tsx +189 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/GrievanceDataSection.tsx +118 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/NominationDataSection.tsx +66 -0
- package/src/RedactoPrivacyCenter/components/DataRequest/PurposeSelectionSection.tsx +305 -0
- package/src/RedactoPrivacyCenter/components/Form.tsx +1455 -0
- package/src/RedactoPrivacyCenter/components/GrievanceTable.tsx +176 -0
- package/src/RedactoPrivacyCenter/components/GrievanceTablePagination.tsx +330 -0
- package/src/RedactoPrivacyCenter/components/ModifyConsentModal.tsx +329 -0
- package/src/RedactoPrivacyCenter/components/Pagination.tsx +140 -0
- package/src/RedactoPrivacyCenter/components/PrivacyCenterLayout.tsx +302 -0
- package/src/RedactoPrivacyCenter/components/RequestSubmit.tsx +218 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideBarPC.tsx +475 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/SideMenuPC.tsx +412 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/constants.tsx +42 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/menuItemsPC.tsx +1 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/types.ts +40 -0
- package/src/RedactoPrivacyCenter/components/SideMenuBarPC/utils.ts +38 -0
- package/src/RedactoPrivacyCenter/components/ThemeTransitionOverlay.tsx +85 -0
- package/src/RedactoPrivacyCenter/components/activitiesColumns.tsx +141 -0
- package/src/RedactoPrivacyCenter/components/caseHistoryColumns.tsx +179 -0
- package/src/RedactoPrivacyCenter/components/constant.ts +20 -0
- package/src/RedactoPrivacyCenter/components/types.ts +68 -0
- package/src/RedactoPrivacyCenter/components/utils.tsx +10 -0
- package/src/RedactoPrivacyCenter/config/routes.ts +9 -0
- package/src/RedactoPrivacyCenter/config/table.ts +25 -0
- package/src/RedactoPrivacyCenter/context/AuthContext.tsx +123 -0
- package/src/RedactoPrivacyCenter/context/BaseUrlContext.tsx +44 -0
- package/src/RedactoPrivacyCenter/context/LocaleContext.tsx +11 -0
- package/src/RedactoPrivacyCenter/context/NavigationContext.tsx +64 -0
- package/src/RedactoPrivacyCenter/context/OrganizationContext.tsx +30 -0
- package/src/RedactoPrivacyCenter/context/PageChromeContext.tsx +60 -0
- package/src/RedactoPrivacyCenter/context/theme.tsx +127 -0
- package/src/RedactoPrivacyCenter/lib/api.ts +468 -0
- package/src/RedactoPrivacyCenter/lib/consent-manager/utils.tsx +63 -0
- package/src/RedactoPrivacyCenter/lib/constants.ts +116 -0
- package/src/RedactoPrivacyCenter/lib/i18n.ts +81 -0
- package/src/RedactoPrivacyCenter/lib/locales.ts +105 -0
- package/src/RedactoPrivacyCenter/lib/queries.ts +30 -0
- package/src/RedactoPrivacyCenter/lib/types.ts +346 -0
- package/src/RedactoPrivacyCenter/lib/useCurrentLanguage.ts +37 -0
- package/src/RedactoPrivacyCenter/lib/utils.ts +625 -0
- package/src/RedactoPrivacyCenter/locales/as/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/bho/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/bn/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/brx/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/doi/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/en/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/gom/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/gu/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/hi/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/kn/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ks/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/mai/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ml/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/mni-Mtei/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/mr/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ne/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/or/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/pa/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/sa/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/sat/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/sd/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ta/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/te/translation.json +188 -0
- package/src/RedactoPrivacyCenter/locales/ur/translation.json +188 -0
- package/src/RedactoPrivacyCenter/styles/base.css +117 -0
- package/src/RedactoPrivacyCenter/styles/injectStyles.ts +1000 -0
- package/src/RedactoPrivacyCenter/styles/pcStyles.ts +819 -0
- package/src/RedactoPrivacyCenter/styles/responsive.ts +138 -0
- package/src/RedactoPrivacyCenter/styles/variables.css +133 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTable.tsx +365 -0
- package/src/RedactoPrivacyCenter/table/AdvancedTablePagination.tsx +479 -0
- package/src/RedactoPrivacyCenter/table/index.ts +2 -0
- package/src/RedactoPrivacyCenter/types/images.d.ts +29 -0
- package/src/RedactoPrivacyCenter/ui/Footer.tsx +73 -0
- package/src/RedactoPrivacyCenter/ui/FormBlockWrapper.tsx +54 -0
- package/src/RedactoPrivacyCenter/ui/PCAccordion.tsx +87 -0
- package/src/RedactoPrivacyCenter/ui/PCActionIcon.tsx +47 -0
- package/src/RedactoPrivacyCenter/ui/PCAvatar.tsx +69 -0
- package/src/RedactoPrivacyCenter/ui/PCBadge.tsx +48 -0
- package/src/RedactoPrivacyCenter/ui/PCButton.tsx +77 -0
- package/src/RedactoPrivacyCenter/ui/PCCheckbox.tsx +55 -0
- package/src/RedactoPrivacyCenter/ui/PCCollapse.tsx +97 -0
- package/src/RedactoPrivacyCenter/ui/PCEmpty.tsx +35 -0
- package/src/RedactoPrivacyCenter/ui/PCInput.tsx +128 -0
- package/src/RedactoPrivacyCenter/ui/PCLoader.tsx +30 -0
- package/src/RedactoPrivacyCenter/ui/PCModal.tsx +104 -0
- package/src/RedactoPrivacyCenter/ui/PCPopover.tsx +175 -0
- package/src/RedactoPrivacyCenter/ui/PCSelect.tsx +381 -0
- package/src/RedactoPrivacyCenter/ui/PCTable.tsx +137 -0
- package/src/RedactoPrivacyCenter/ui/PCText.tsx +39 -0
- package/src/RedactoPrivacyCenter/ui/PCTextarea.tsx +43 -0
- package/src/RedactoPrivacyCenter/ui/PCTitle.tsx +37 -0
- package/src/RedactoPrivacyCenter/ui/PCTooltip.tsx +119 -0
- package/src/RedactoPrivacyCenter/ui/SimpleBar.tsx +25 -0
- package/src/RedactoPrivacyCenter/ui/StatusBadge.tsx +55 -0
- package/src/RedactoPrivacyCenter/ui/ThemeToggle.tsx +70 -0
- package/src/RedactoPrivacyCenter/ui/index.ts +18 -0
- package/src/RedactoPrivacyCenter/utils/cn.ts +28 -0
- package/src/RedactoPrivacyCenter/utils/date.ts +48 -0
- package/src/RedactoPrivacyCenter/utils/status.ts +111 -0
- package/src/RedactoPrivacyCenter/utils/toast.ts +130 -0
- package/src/index.ts +10 -0
- package/src/privacy-center.ts +107 -0
- package/tsconfig.json +2 -0
- package/tsup.config.ts +16 -1
- package/src/utils/isLegacyApi.ts +0 -12
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, type ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type InternalPage = 'consent-manager' | 'form' | 'activity';
|
|
4
|
+
|
|
5
|
+
const NAV_STORAGE_KEY = 'pc_current_page';
|
|
6
|
+
|
|
7
|
+
const VALID_PAGES: ReadonlySet<string> = new Set<InternalPage>([
|
|
8
|
+
'consent-manager', 'form', 'activity',
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function getStoredPage(fallback: InternalPage): InternalPage {
|
|
12
|
+
if (typeof window === 'undefined') return fallback;
|
|
13
|
+
try {
|
|
14
|
+
const stored = sessionStorage.getItem(NAV_STORAGE_KEY);
|
|
15
|
+
if (stored && VALID_PAGES.has(stored)) {
|
|
16
|
+
return stored as InternalPage;
|
|
17
|
+
}
|
|
18
|
+
} catch {
|
|
19
|
+
// sessionStorage unavailable
|
|
20
|
+
}
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface NavigationContextType {
|
|
25
|
+
currentPage: InternalPage;
|
|
26
|
+
navigateTo: (page: InternalPage) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const NavigationContext = createContext<NavigationContextType | null>(null);
|
|
30
|
+
|
|
31
|
+
export const NavigationProvider = ({
|
|
32
|
+
children,
|
|
33
|
+
initialPage = 'consent-manager'
|
|
34
|
+
}: {
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
initialPage?: InternalPage;
|
|
37
|
+
}) => {
|
|
38
|
+
const [currentPage, setCurrentPage] = useState<InternalPage>(
|
|
39
|
+
() => getStoredPage(initialPage)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const navigateTo = useCallback((page: InternalPage) => {
|
|
43
|
+
setCurrentPage(page);
|
|
44
|
+
try {
|
|
45
|
+
sessionStorage.setItem(NAV_STORAGE_KEY, page);
|
|
46
|
+
} catch {
|
|
47
|
+
// sessionStorage unavailable
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<NavigationContext.Provider value={{ currentPage, navigateTo }}>
|
|
53
|
+
{children}
|
|
54
|
+
</NavigationContext.Provider>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const useInternalNavigation = () => {
|
|
59
|
+
const context = useContext(NavigationContext);
|
|
60
|
+
if (!context) {
|
|
61
|
+
throw new Error('useInternalNavigation must be used within a NavigationProvider');
|
|
62
|
+
}
|
|
63
|
+
return context;
|
|
64
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { createContext, useContext, ReactNode } from 'react';
|
|
2
|
+
import type { OrganizationDetail } from '../lib/types';
|
|
3
|
+
|
|
4
|
+
export const OrganizationContext = createContext<{
|
|
5
|
+
organization: OrganizationDetail | null;
|
|
6
|
+
} | null>(null);
|
|
7
|
+
|
|
8
|
+
export const OrganizationProvider = ({
|
|
9
|
+
children,
|
|
10
|
+
organization,
|
|
11
|
+
}: {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
organization: OrganizationDetail | null;
|
|
14
|
+
}) => {
|
|
15
|
+
return (
|
|
16
|
+
<OrganizationContext.Provider value={{ organization }}>
|
|
17
|
+
{children}
|
|
18
|
+
</OrganizationContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const useOrganization = () => {
|
|
23
|
+
const context = useContext(OrganizationContext);
|
|
24
|
+
if (!context) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'useOrganization must be used within a OrganizationProvider'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return context;
|
|
30
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useState,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
export type PageChromeOverride = {
|
|
11
|
+
titleKey: string;
|
|
12
|
+
descriptionKey?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type PageChromeContextValue = {
|
|
16
|
+
pageChromeOverride: PageChromeOverride | null;
|
|
17
|
+
setPageChromeOverride: (override: PageChromeOverride | null) => void;
|
|
18
|
+
clearPageChromeOverride: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PageChromeContext = createContext<PageChromeContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
export function PageChromeProvider({ children }: { children: ReactNode }) {
|
|
24
|
+
const [pageChromeOverride, setPageChromeOverrideState] =
|
|
25
|
+
useState<PageChromeOverride | null>(null);
|
|
26
|
+
|
|
27
|
+
const setPageChromeOverride = useCallback(
|
|
28
|
+
(override: PageChromeOverride | null) => {
|
|
29
|
+
setPageChromeOverrideState(override);
|
|
30
|
+
},
|
|
31
|
+
[]
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const clearPageChromeOverride = useCallback(() => {
|
|
35
|
+
setPageChromeOverrideState(null);
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
const value = useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
pageChromeOverride,
|
|
41
|
+
setPageChromeOverride,
|
|
42
|
+
clearPageChromeOverride,
|
|
43
|
+
}),
|
|
44
|
+
[pageChromeOverride, setPageChromeOverride, clearPageChromeOverride]
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<PageChromeContext.Provider value={value}>
|
|
49
|
+
{children}
|
|
50
|
+
</PageChromeContext.Provider>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function usePageChrome() {
|
|
55
|
+
const context = useContext(PageChromeContext);
|
|
56
|
+
if (!context) {
|
|
57
|
+
throw new Error('usePageChrome must be used within a PageChromeProvider');
|
|
58
|
+
}
|
|
59
|
+
return context;
|
|
60
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
useState,
|
|
5
|
+
useEffect,
|
|
6
|
+
useCallback,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
export type Theme = 'light' | 'dark';
|
|
11
|
+
|
|
12
|
+
type ThemeContextType = {
|
|
13
|
+
theme: Theme;
|
|
14
|
+
toggleTheme: () => void;
|
|
15
|
+
setTheme: (theme: Theme) => void;
|
|
16
|
+
isTransitioning: boolean;
|
|
17
|
+
targetTheme: Theme | null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const THEME_STORAGE_KEY = 'privacy-center-theme';
|
|
21
|
+
|
|
22
|
+
const ThemeContext = createContext<ThemeContextType | null>(null);
|
|
23
|
+
|
|
24
|
+
function getInitialTheme(defaultTheme?: Theme): Theme {
|
|
25
|
+
// If defaultTheme is provided, always use it (takes precedence over sessionStorage)
|
|
26
|
+
// This ensures that when a parent component explicitly sets a theme, it's respected
|
|
27
|
+
if (defaultTheme !== undefined) {
|
|
28
|
+
return defaultTheme;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Otherwise, check sessionStorage first
|
|
32
|
+
if (typeof window !== 'undefined') {
|
|
33
|
+
const stored = sessionStorage.getItem(THEME_STORAGE_KEY);
|
|
34
|
+
if (stored === 'light' || stored === 'dark') {
|
|
35
|
+
return stored;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback to system preference
|
|
39
|
+
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
40
|
+
return 'dark';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return 'light';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type ThemeProviderProps = {
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
defaultTheme?: Theme;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const ThemeProvider = ({ children, defaultTheme }: ThemeProviderProps) => {
|
|
53
|
+
// Initialize with defaultTheme if provided, otherwise use stored/system preference
|
|
54
|
+
// When defaultTheme is provided, it takes precedence over sessionStorage
|
|
55
|
+
const [theme, setThemeState] = useState<Theme>(() => getInitialTheme(defaultTheme));
|
|
56
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
57
|
+
const [targetTheme, setTargetTheme] = useState<Theme | null>(null);
|
|
58
|
+
|
|
59
|
+
// Ensure defaultTheme is applied on mount if provided
|
|
60
|
+
// This handles the case where sessionStorage had a different value
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (defaultTheme !== undefined) {
|
|
63
|
+
setThemeState(defaultTheme);
|
|
64
|
+
}
|
|
65
|
+
}, []); // Only run on mount
|
|
66
|
+
|
|
67
|
+
const setTheme = useCallback((newTheme: Theme) => {
|
|
68
|
+
if (newTheme !== theme) {
|
|
69
|
+
setIsTransitioning(true);
|
|
70
|
+
setTargetTheme(newTheme);
|
|
71
|
+
setThemeState(newTheme);
|
|
72
|
+
// Reset transition state after animation completes
|
|
73
|
+
setTimeout(() => {
|
|
74
|
+
setIsTransitioning(false);
|
|
75
|
+
setTargetTheme(null);
|
|
76
|
+
}, 1000);
|
|
77
|
+
}
|
|
78
|
+
}, [theme]);
|
|
79
|
+
|
|
80
|
+
const toggleTheme = useCallback(() => {
|
|
81
|
+
setIsTransitioning(true);
|
|
82
|
+
const newTheme = theme === 'light' ? 'dark' : 'light';
|
|
83
|
+
setTargetTheme(newTheme);
|
|
84
|
+
setThemeState(newTheme);
|
|
85
|
+
// Reset transition state after animation completes
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
setIsTransitioning(false);
|
|
88
|
+
setTargetTheme(null);
|
|
89
|
+
}, 1000);
|
|
90
|
+
}, [theme]);
|
|
91
|
+
|
|
92
|
+
// Apply theme to document and persist to sessionStorage
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
document.documentElement.setAttribute('data-theme', theme);
|
|
95
|
+
sessionStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
96
|
+
}, [theme]);
|
|
97
|
+
|
|
98
|
+
// Listen for system theme changes
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
101
|
+
|
|
102
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
103
|
+
// Only auto-switch if user hasn't explicitly set a preference
|
|
104
|
+
const stored = sessionStorage.getItem(THEME_STORAGE_KEY);
|
|
105
|
+
if (!stored) {
|
|
106
|
+
setThemeState(e.matches ? 'dark' : 'light');
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
mediaQuery.addEventListener('change', handleChange);
|
|
111
|
+
return () => mediaQuery.removeEventListener('change', handleChange);
|
|
112
|
+
}, []);
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme, isTransitioning, targetTheme }}>
|
|
116
|
+
{children}
|
|
117
|
+
</ThemeContext.Provider>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export const useTheme = (): ThemeContextType => {
|
|
122
|
+
const context = useContext(ThemeContext);
|
|
123
|
+
if (!context) {
|
|
124
|
+
throw new Error('useTheme must be used within a ThemeProvider');
|
|
125
|
+
}
|
|
126
|
+
return context;
|
|
127
|
+
};
|