@rebasepro/app 0.0.1-canary.4829d6e
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/LICENSE +21 -0
- package/README.md +174 -0
- package/dist/auth/api.d.ts +66 -0
- package/dist/auth/index.d.ts +13 -0
- package/dist/auth/types.d.ts +127 -0
- package/dist/auth/useRebaseAuthController.d.ts +13 -0
- package/dist/components/AIIcon.d.ts +17 -0
- package/dist/components/AuthSimulationMenu.d.ts +0 -0
- package/dist/components/ConfirmationDialog.d.ts +9 -0
- package/dist/components/Debug/UIReferenceView.d.ts +13 -0
- package/dist/components/Debug/UIStyleGuide.d.ts +2 -0
- package/dist/components/Debug/collection-views/CardViewDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/CollectionTableDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/KanbanBoardDemo.d.ts +2 -0
- package/dist/components/Debug/collection-views/index.d.ts +3 -0
- package/dist/components/Debug/collection-views/sample_data.d.ts +23 -0
- package/dist/components/Debug/crm-dashboard/CalendarWidget.d.ts +22 -0
- package/dist/components/Debug/crm-dashboard/CrmDashboardDemo.d.ts +2 -0
- package/dist/components/Debug/crm-dashboard/DashboardMetrics.d.ts +15 -0
- package/dist/components/Debug/crm-dashboard/PipelineOverview.d.ts +34 -0
- package/dist/components/Debug/crm-dashboard/RecentActivity.d.ts +19 -0
- package/dist/components/Debug/crm-dashboard/TaskFilters.d.ts +30 -0
- package/dist/components/Debug/crm-dashboard/TaskTable.d.ts +50 -0
- package/dist/components/Debug/crm-dashboard/TasksView.d.ts +41 -0
- package/dist/components/ErrorTooltip.d.ts +3 -0
- package/dist/components/ErrorView.d.ts +21 -0
- package/dist/components/LanguageToggle.d.ts +2 -0
- package/dist/components/LoginView/LoginView.d.ts +113 -0
- package/dist/components/LoginView/index.d.ts +2 -0
- package/dist/components/NotFoundPage.d.ts +2 -0
- package/dist/components/RebaseAuth.d.ts +10 -0
- package/dist/components/RebaseLogo.d.ts +7 -0
- package/dist/components/SchemaDriftBanner.d.ts +27 -0
- package/dist/components/UnsavedChangesDialog.d.ts +9 -0
- package/dist/components/UserDisplay.d.ts +7 -0
- package/dist/components/UserSelectPopover.d.ts +63 -0
- package/dist/components/UserSettingsView.d.ts +2 -0
- package/dist/components/common/index.d.ts +6 -0
- package/dist/components/common/table_height.d.ts +5 -0
- package/dist/components/common/types.d.ts +66 -0
- package/dist/components/common/useColumnsIds.d.ts +9 -0
- package/dist/components/common/useDataTableController.d.ts +45 -0
- package/dist/components/common/useDebouncedData.d.ts +9 -0
- package/dist/components/common/useScrollRestoration.d.ts +14 -0
- package/dist/components/index.d.ts +18 -0
- package/dist/contexts/AdminModeController.d.ts +4 -0
- package/dist/contexts/AnalyticsContext.d.ts +3 -0
- package/dist/contexts/AuthControllerContext.d.ts +3 -0
- package/dist/contexts/CollectionScopeContext.d.ts +50 -0
- package/dist/contexts/ComponentOverrideContext.d.ts +42 -0
- package/dist/contexts/CustomizationControllerContext.d.ts +3 -0
- package/dist/contexts/DataDriverContext.d.ts +3 -0
- package/dist/contexts/DataSourcesContext.d.ts +31 -0
- package/dist/contexts/DatabaseAdminContext.d.ts +3 -0
- package/dist/contexts/DialogsProvider.d.ts +4 -0
- package/dist/contexts/EffectiveRoleController.d.ts +4 -0
- package/dist/contexts/ModeController.d.ts +4 -0
- package/dist/contexts/RebaseClientInstanceContext.d.ts +6 -0
- package/dist/contexts/RebaseDataContext.d.ts +3 -0
- package/dist/contexts/SnackbarProvider.d.ts +2 -0
- package/dist/contexts/StorageSourceContext.d.ts +3 -0
- package/dist/contexts/StorageSourcesContext.d.ts +25 -0
- package/dist/contexts/UserConfigurationPersistenceContext.d.ts +3 -0
- package/dist/contexts/index.d.ts +17 -0
- package/dist/core/PluginLifecycleManager.d.ts +17 -0
- package/dist/core/PluginProviderStack.d.ts +21 -0
- package/dist/core/Rebase.d.ts +15 -0
- package/dist/core/RebaseProps.d.ts +291 -0
- package/dist/core/RebaseRouter.d.ts +4 -0
- package/dist/core/RebaseRoutes.d.ts +17 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/hooks/ApiConfigContext.d.ts +24 -0
- package/dist/hooks/data/delete.d.ts +33 -0
- package/dist/hooks/data/save.d.ts +40 -0
- package/dist/hooks/data/useCollection.d.ts +62 -0
- package/dist/hooks/data/useData.d.ts +13 -0
- package/dist/hooks/data/useDataOrder.d.ts +12 -0
- package/dist/hooks/data/useFetch.d.ts +43 -0
- package/dist/hooks/data/useRelationSelector.d.ts +52 -0
- package/dist/hooks/index.d.ts +36 -0
- package/dist/hooks/useAdminModeController.d.ts +19 -0
- package/dist/hooks/useAnalyticsController.d.ts +5 -0
- package/dist/hooks/useAuthController.d.ts +11 -0
- package/dist/hooks/useAuthSubscription.d.ts +2 -0
- package/dist/hooks/useBackendStorageSource.d.ts +30 -0
- package/dist/hooks/useBridgeRegistration.d.ts +18 -0
- package/dist/hooks/useBrowserTitleAndIcon.d.ts +6 -0
- package/dist/hooks/useBuildAdminModeController.d.ts +6 -0
- package/dist/hooks/useBuildEffectiveRoleController.d.ts +8 -0
- package/dist/hooks/useBuildLocalConfigurationPersistence.d.ts +2 -0
- package/dist/hooks/useBuildModeController.d.ts +6 -0
- package/dist/hooks/useClipboard.d.ts +57 -0
- package/dist/hooks/useCollapsedGroups.d.ts +27 -0
- package/dist/hooks/useComponentOverride.d.ts +32 -0
- package/dist/hooks/useCustomizationController.d.ts +11 -0
- package/dist/hooks/useDialogsController.d.ts +11 -0
- package/dist/hooks/useEffectiveRoleController.d.ts +7 -0
- package/dist/hooks/useLargeLayout.d.ts +1 -0
- package/dist/hooks/useModeController.d.ts +19 -0
- package/dist/hooks/usePermissions.d.ts +12 -0
- package/dist/hooks/useRebaseClient.d.ts +5 -0
- package/dist/hooks/useRebaseContext.d.ts +11 -0
- package/dist/hooks/useRebaseRegistry.d.ts +34 -0
- package/dist/hooks/useResolvedComponent.d.ts +47 -0
- package/dist/hooks/useSlot.d.ts +18 -0
- package/dist/hooks/useSnackbarController.d.ts +20 -0
- package/dist/hooks/useStorageSource.d.ts +6 -0
- package/dist/hooks/useStudioBridge.d.ts +79 -0
- package/dist/hooks/useTranslation.d.ts +17 -0
- package/dist/hooks/useUnsavedChangesDialog.d.ts +12 -0
- package/dist/hooks/useUserConfigurationPersistence.d.ts +8 -0
- package/dist/i18n/RebaseI18nProvider.d.ts +33 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.es.js +16843 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +17021 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/internal/common.d.ts +14 -0
- package/dist/internal/useRestoreScroll.d.ts +15 -0
- package/dist/locales/de.d.ts +2 -0
- package/dist/locales/en.d.ts +10 -0
- package/dist/locales/es.d.ts +10 -0
- package/dist/locales/fr.d.ts +2 -0
- package/dist/locales/hi.d.ts +2 -0
- package/dist/locales/it.d.ts +2 -0
- package/dist/locales/pt.d.ts +7 -0
- package/dist/util/constants.d.ts +1 -0
- package/dist/util/createFormexStub.d.ts +2 -0
- package/dist/util/entity_cache.d.ts +22 -0
- package/dist/util/enums.d.ts +5 -0
- package/dist/util/icon_list.d.ts +5 -0
- package/dist/util/icons.d.ts +20 -0
- package/dist/util/index.d.ts +8 -0
- package/dist/util/previews.d.ts +4 -0
- package/dist/util/useStorageUploadController.d.ts +44 -0
- package/dist/vitePlugin.d.ts +60 -0
- package/dist/vitePlugin.js +113 -0
- package/package.json +145 -0
- package/src/auth/api.ts +148 -0
- package/src/auth/index.ts +25 -0
- package/src/auth/types.ts +115 -0
- package/src/auth/useRebaseAuthController.ts +328 -0
- package/src/components/AIIcon.tsx +47 -0
- package/src/components/AuthSimulationMenu.tsx +0 -0
- package/src/components/ConfirmationDialog.tsx +48 -0
- package/src/components/Debug/UIReferenceView.tsx +958 -0
- package/src/components/Debug/UIStyleGuide.tsx +156 -0
- package/src/components/Debug/collection-views/CardViewDemo.tsx +85 -0
- package/src/components/Debug/collection-views/CollectionTableDemo.tsx +107 -0
- package/src/components/Debug/collection-views/KanbanBoardDemo.tsx +90 -0
- package/src/components/Debug/collection-views/index.ts +3 -0
- package/src/components/Debug/collection-views/sample_data.ts +42 -0
- package/src/components/Debug/crm-dashboard/CalendarWidget.tsx +389 -0
- package/src/components/Debug/crm-dashboard/CrmDashboardDemo.tsx +1102 -0
- package/src/components/Debug/crm-dashboard/DashboardMetrics.tsx +133 -0
- package/src/components/Debug/crm-dashboard/PipelineOverview.tsx +290 -0
- package/src/components/Debug/crm-dashboard/RecentActivity.tsx +215 -0
- package/src/components/Debug/crm-dashboard/TaskFilters.tsx +184 -0
- package/src/components/Debug/crm-dashboard/TaskTable.tsx +427 -0
- package/src/components/Debug/crm-dashboard/TasksView.tsx +234 -0
- package/src/components/ErrorTooltip.tsx +12 -0
- package/src/components/ErrorView.tsx +94 -0
- package/src/components/LanguageToggle.tsx +59 -0
- package/src/components/LoginView/LoginView.tsx +932 -0
- package/src/components/LoginView/index.ts +2 -0
- package/src/components/NotFoundPage.tsx +27 -0
- package/src/components/RebaseAuth.tsx +27 -0
- package/src/components/RebaseLogo.tsx +29 -0
- package/src/components/SchemaDriftBanner.tsx +102 -0
- package/src/components/UnsavedChangesDialog.tsx +46 -0
- package/src/components/UserDisplay.tsx +54 -0
- package/src/components/UserSelectPopover.tsx +399 -0
- package/src/components/UserSettingsView.tsx +318 -0
- package/src/components/common/index.ts +6 -0
- package/src/components/common/table_height.tsx +21 -0
- package/src/components/common/types.tsx +66 -0
- package/src/components/common/useColumnsIds.tsx +222 -0
- package/src/components/common/useDataTableController.tsx +514 -0
- package/src/components/common/useDebouncedData.ts +49 -0
- package/src/components/common/useScrollRestoration.tsx +68 -0
- package/src/components/index.tsx +26 -0
- package/src/contexts/AdminModeController.tsx +11 -0
- package/src/contexts/AnalyticsContext.tsx +4 -0
- package/src/contexts/AuthControllerContext.tsx +4 -0
- package/src/contexts/CollectionScopeContext.tsx +67 -0
- package/src/contexts/ComponentOverrideContext.tsx +81 -0
- package/src/contexts/CustomizationControllerContext.tsx +4 -0
- package/src/contexts/DataDriverContext.tsx +4 -0
- package/src/contexts/DataSourcesContext.tsx +36 -0
- package/src/contexts/DatabaseAdminContext.tsx +4 -0
- package/src/contexts/DialogsProvider.tsx +56 -0
- package/src/contexts/EffectiveRoleController.tsx +12 -0
- package/src/contexts/ModeController.tsx +11 -0
- package/src/contexts/RebaseClientInstanceContext.tsx +7 -0
- package/src/contexts/RebaseDataContext.tsx +4 -0
- package/src/contexts/SnackbarProvider.tsx +15 -0
- package/src/contexts/StorageSourceContext.tsx +4 -0
- package/src/contexts/StorageSourcesContext.tsx +30 -0
- package/src/contexts/UserConfigurationPersistenceContext.tsx +4 -0
- package/src/contexts/index.ts +17 -0
- package/src/core/PluginLifecycleManager.tsx +95 -0
- package/src/core/PluginProviderStack.tsx +40 -0
- package/src/core/Rebase.tsx +439 -0
- package/src/core/RebaseProps.tsx +320 -0
- package/src/core/RebaseRouter.tsx +17 -0
- package/src/core/RebaseRoutes.tsx +24 -0
- package/src/core/index.tsx +5 -0
- package/src/hooks/ApiConfigContext.tsx +41 -0
- package/src/hooks/data/delete.ts +55 -0
- package/src/hooks/data/save.ts +77 -0
- package/src/hooks/data/useCollection.tsx +176 -0
- package/src/hooks/data/useData.tsx +18 -0
- package/src/hooks/data/useDataOrder.ts +26 -0
- package/src/hooks/data/useFetch.tsx +140 -0
- package/src/hooks/data/useRelationSelector.tsx +234 -0
- package/src/hooks/index.tsx +45 -0
- package/src/hooks/useAdminModeController.tsx +23 -0
- package/src/hooks/useAnalyticsController.tsx +8 -0
- package/src/hooks/useAuthController.tsx +14 -0
- package/src/hooks/useAuthSubscription.ts +87 -0
- package/src/hooks/useBackendStorageSource.ts +288 -0
- package/src/hooks/useBridgeRegistration.tsx +32 -0
- package/src/hooks/useBrowserTitleAndIcon.tsx +23 -0
- package/src/hooks/useBuildAdminModeController.tsx +24 -0
- package/src/hooks/useBuildEffectiveRoleController.tsx +31 -0
- package/src/hooks/useBuildLocalConfigurationPersistence.tsx +75 -0
- package/src/hooks/useBuildModeController.tsx +71 -0
- package/src/hooks/useClipboard.tsx +158 -0
- package/src/hooks/useCollapsedGroups.ts +116 -0
- package/src/hooks/useComponentOverride.tsx +59 -0
- package/src/hooks/useCustomizationController.tsx +14 -0
- package/src/hooks/useDialogsController.tsx +15 -0
- package/src/hooks/useEffectiveRoleController.tsx +10 -0
- package/src/hooks/useLargeLayout.tsx +65 -0
- package/src/hooks/useModeController.tsx +23 -0
- package/src/hooks/usePermissions.ts +44 -0
- package/src/hooks/useRebaseClient.tsx +10 -0
- package/src/hooks/useRebaseContext.tsx +83 -0
- package/src/hooks/useRebaseRegistry.tsx +91 -0
- package/src/hooks/useResolvedComponent.tsx +159 -0
- package/src/hooks/useSlot.tsx +64 -0
- package/src/hooks/useSnackbarController.tsx +52 -0
- package/src/hooks/useStorageSource.tsx +11 -0
- package/src/hooks/useStudioBridge.tsx +209 -0
- package/src/hooks/useTranslation.ts +33 -0
- package/src/hooks/useUnsavedChangesDialog.tsx +64 -0
- package/src/hooks/useUserConfigurationPersistence.tsx +11 -0
- package/src/i18n/RebaseI18nProvider.tsx +160 -0
- package/src/index.ts +25 -0
- package/src/internal/common.tsx +16 -0
- package/src/internal/useRestoreScroll.tsx +69 -0
- package/src/locales/de.ts +902 -0
- package/src/locales/en.ts +980 -0
- package/src/locales/es.ts +922 -0
- package/src/locales/fr.ts +901 -0
- package/src/locales/hi.ts +901 -0
- package/src/locales/it.ts +901 -0
- package/src/locales/pt.ts +909 -0
- package/src/util/constants.ts +7 -0
- package/src/util/createFormexStub.tsx +66 -0
- package/src/util/entity_cache.ts +239 -0
- package/src/util/enums.ts +32 -0
- package/src/util/icon_list.ts +22 -0
- package/src/util/icons.tsx +149 -0
- package/src/util/index.ts +12 -0
- package/src/util/previews.ts +105 -0
- package/src/util/useStorageUploadController.tsx +350 -0
- package/src/vitePlugin.ts +196 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
2
|
+
import { AuthClient, AuthController, User } from "@rebasepro/types";
|
|
3
|
+
|
|
4
|
+
export function useAuthSubscription(authClient?: AuthClient): AuthController {
|
|
5
|
+
|
|
6
|
+
// Check initial state
|
|
7
|
+
const currentSession = authClient?.getSession();
|
|
8
|
+
const [user, setUser] = useState<User | null>(currentSession?.user ?? null);
|
|
9
|
+
|
|
10
|
+
const [initialLoading, setInitialLoading] = useState(!currentSession);
|
|
11
|
+
const [authLoading, setAuthLoading] = useState(false);
|
|
12
|
+
const [authError, setAuthError] = useState<Error>();
|
|
13
|
+
const [loginSkipped, setLoginSkipped] = useState(false);
|
|
14
|
+
const [extra, setExtra] = useState<any>();
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (!authClient) return;
|
|
18
|
+
// If we don't have a session initially, try to get user which restores session if a persistent token exists
|
|
19
|
+
if (!currentSession) {
|
|
20
|
+
setInitialLoading(true);
|
|
21
|
+
authClient.getUser()
|
|
22
|
+
.then(user => {
|
|
23
|
+
if (user) {
|
|
24
|
+
setUser(user);
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.catch(e => {
|
|
28
|
+
// Ignore, user just isn't logged in
|
|
29
|
+
})
|
|
30
|
+
.finally(() => {
|
|
31
|
+
setInitialLoading(false);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}, [authClient, currentSession]);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!authClient) return;
|
|
38
|
+
const unsubscribe = authClient.onAuthStateChange((event, session) => {
|
|
39
|
+
if (event === "SIGNED_IN" || event === "USER_UPDATED" || event === "TOKEN_REFRESHED") {
|
|
40
|
+
setUser(session?.user ?? null);
|
|
41
|
+
} else if (event === "SIGNED_OUT") {
|
|
42
|
+
setUser(null);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return unsubscribe;
|
|
46
|
+
}, [authClient]);
|
|
47
|
+
|
|
48
|
+
const signOut = useCallback(async () => {
|
|
49
|
+
if (!authClient) return;
|
|
50
|
+
setAuthLoading(true);
|
|
51
|
+
try {
|
|
52
|
+
await authClient.signOut();
|
|
53
|
+
} finally {
|
|
54
|
+
setAuthLoading(false);
|
|
55
|
+
}
|
|
56
|
+
}, [authClient]);
|
|
57
|
+
|
|
58
|
+
const getAuthToken = useCallback(async () => {
|
|
59
|
+
if (!authClient) return "";
|
|
60
|
+
const session = authClient.getSession();
|
|
61
|
+
if (!session) return "";
|
|
62
|
+
if (session.expiresAt < Date.now()) {
|
|
63
|
+
try {
|
|
64
|
+
const refreshed = await authClient.refreshSession();
|
|
65
|
+
return refreshed.accessToken;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
return "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return session.accessToken;
|
|
71
|
+
}, [authClient]);
|
|
72
|
+
|
|
73
|
+
return useMemo(() => ({
|
|
74
|
+
user,
|
|
75
|
+
initialLoading: authClient ? initialLoading : false,
|
|
76
|
+
authLoading,
|
|
77
|
+
signOut,
|
|
78
|
+
authError,
|
|
79
|
+
getAuthToken,
|
|
80
|
+
loginSkipped,
|
|
81
|
+
extra,
|
|
82
|
+
setExtra
|
|
83
|
+
} as AuthController), [
|
|
84
|
+
user, initialLoading, authLoading, signOut, authError,
|
|
85
|
+
getAuthToken, loginSkipped, extra, setExtra, authClient
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for using backend storage API as a StorageSource
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useMemo, useCallback } from "react";
|
|
6
|
+
import {
|
|
7
|
+
StorageSource,
|
|
8
|
+
UploadFileProps,
|
|
9
|
+
UploadFileResult,
|
|
10
|
+
DownloadConfig,
|
|
11
|
+
StorageListResult
|
|
12
|
+
} from "@rebasepro/types";
|
|
13
|
+
|
|
14
|
+
export interface BackendStorageSourceProps {
|
|
15
|
+
/**
|
|
16
|
+
* Backend API URL (e.g., 'http://localhost:3001')
|
|
17
|
+
*/
|
|
18
|
+
apiUrl: string;
|
|
19
|
+
/**
|
|
20
|
+
* Function to get the current auth token
|
|
21
|
+
*/
|
|
22
|
+
getAuthToken: () => Promise<string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook to create a StorageSource that uses the backend storage REST API.
|
|
27
|
+
* Use this for self-hosted Rebase with local or S3 storage.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const storageSource = useBackendStorageSource({
|
|
32
|
+
* apiUrl: 'http://localhost:3001',
|
|
33
|
+
* getAuthToken: authController.getAuthToken
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // Then pass to Rebase:
|
|
37
|
+
* <Rebase storageSource={storageSource} ... />
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export function useBackendStorageSource({
|
|
41
|
+
apiUrl,
|
|
42
|
+
getAuthToken
|
|
43
|
+
}: BackendStorageSourceProps): StorageSource {
|
|
44
|
+
|
|
45
|
+
const storageBasePath = `${apiUrl}/api/storage`;
|
|
46
|
+
|
|
47
|
+
// Cache for download URLs to avoid redundant API calls
|
|
48
|
+
const urlsCache = useMemo(() => new Map<string, DownloadConfig>(), []);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Make an authenticated request to the storage API
|
|
52
|
+
*/
|
|
53
|
+
const fetchWithAuth = useCallback(async (
|
|
54
|
+
url: string,
|
|
55
|
+
options: RequestInit = {}
|
|
56
|
+
): Promise<Response> => {
|
|
57
|
+
const token = await getAuthToken();
|
|
58
|
+
return fetch(url, {
|
|
59
|
+
...options,
|
|
60
|
+
headers: {
|
|
61
|
+
...options.headers,
|
|
62
|
+
"Authorization": `Bearer ${token}`
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}, [getAuthToken]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Upload a file to storage
|
|
69
|
+
*/
|
|
70
|
+
const putObject = useCallback(async ({
|
|
71
|
+
file,
|
|
72
|
+
key,
|
|
73
|
+
metadata,
|
|
74
|
+
bucket
|
|
75
|
+
}: UploadFileProps): Promise<UploadFileResult> => {
|
|
76
|
+
const formData = new FormData();
|
|
77
|
+
formData.append("file", file);
|
|
78
|
+
|
|
79
|
+
if (key) {
|
|
80
|
+
formData.append("key", key);
|
|
81
|
+
}
|
|
82
|
+
if (bucket) {
|
|
83
|
+
formData.append("bucket", bucket);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Add metadata fields with prefix
|
|
87
|
+
if (metadata) {
|
|
88
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
89
|
+
if (value !== undefined && value !== null) {
|
|
90
|
+
formData.append(
|
|
91
|
+
`metadata_${key}`,
|
|
92
|
+
typeof value === "string" ? value : JSON.stringify(value)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const response = await fetchWithAuth(`${storageBasePath}/upload`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: formData
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
const error = await response.json().catch(() => ({ error: "Upload failed" }));
|
|
105
|
+
throw new Error(error.error || "Upload failed");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = await response.json();
|
|
109
|
+
return result.data;
|
|
110
|
+
}, [fetchWithAuth, storageBasePath]);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get download URL for a file
|
|
114
|
+
*/
|
|
115
|
+
const getSignedUrl = useCallback(async (
|
|
116
|
+
keyOrUrl: string,
|
|
117
|
+
bucket?: string
|
|
118
|
+
): Promise<DownloadConfig> => {
|
|
119
|
+
// Check cache first
|
|
120
|
+
const cacheKey = bucket ? `${bucket}/${keyOrUrl}` : keyOrUrl;
|
|
121
|
+
const cached = urlsCache.get(cacheKey);
|
|
122
|
+
if (cached) {
|
|
123
|
+
return cached;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Build the file path for the API
|
|
127
|
+
let filePath = keyOrUrl;
|
|
128
|
+
|
|
129
|
+
// Handle local:// and s3:// URLs
|
|
130
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
131
|
+
const withoutProtocol = filePath.substring(filePath.indexOf("://") + 3);
|
|
132
|
+
filePath = withoutProtocol;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If bucket is provided separately, prepend it
|
|
136
|
+
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
137
|
+
filePath = `${bucket}/${filePath}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
141
|
+
return { url: null,
|
|
142
|
+
fileNotFound: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const response = await fetchWithAuth(`${storageBasePath}/metadata/${filePath}`);
|
|
146
|
+
|
|
147
|
+
if (response.status === 404) {
|
|
148
|
+
return {
|
|
149
|
+
url: null,
|
|
150
|
+
fileNotFound: true
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const error = await response.json().catch(() => ({ error: "Failed to get download URL" }));
|
|
156
|
+
throw new Error(error.error || "Failed to get download URL");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await response.json();
|
|
160
|
+
|
|
161
|
+
const token = await getAuthToken();
|
|
162
|
+
const tokenQuery = token ? `?token=${token}` : "";
|
|
163
|
+
|
|
164
|
+
// The URL should point to the storage file endpoint
|
|
165
|
+
const downloadConfig: DownloadConfig = {
|
|
166
|
+
url: `${storageBasePath}/file/${filePath}${tokenQuery}`,
|
|
167
|
+
metadata: result.data
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Cache the result
|
|
171
|
+
urlsCache.set(cacheKey, downloadConfig);
|
|
172
|
+
|
|
173
|
+
return downloadConfig;
|
|
174
|
+
}, [fetchWithAuth, storageBasePath, urlsCache]);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get file as a File object
|
|
178
|
+
*/
|
|
179
|
+
const getObject = useCallback(async (
|
|
180
|
+
key: string,
|
|
181
|
+
bucket?: string
|
|
182
|
+
): Promise<File | null> => {
|
|
183
|
+
let filePath = key;
|
|
184
|
+
|
|
185
|
+
// Handle protocol URLs
|
|
186
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
187
|
+
const withoutProtocol = filePath.substring(filePath.indexOf("://") + 3);
|
|
188
|
+
filePath = withoutProtocol;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
192
|
+
filePath = `${bucket}/${filePath}`;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const response = await fetchWithAuth(`${storageBasePath}/file/${filePath}`);
|
|
200
|
+
|
|
201
|
+
if (response.status === 404) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
throw new Error("Failed to get file");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const blob = await response.blob();
|
|
210
|
+
const fileName = filePath.split("/").pop() || "file";
|
|
211
|
+
return new File([blob], fileName, { type: blob.type });
|
|
212
|
+
}, [fetchWithAuth, storageBasePath]);
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Delete a file
|
|
216
|
+
*/
|
|
217
|
+
const deleteObject = useCallback(async (
|
|
218
|
+
key: string,
|
|
219
|
+
bucket?: string
|
|
220
|
+
): Promise<void> => {
|
|
221
|
+
let filePath = key;
|
|
222
|
+
|
|
223
|
+
// Handle protocol URLs
|
|
224
|
+
if (filePath && (filePath.startsWith("local://") || filePath.startsWith("s3://"))) {
|
|
225
|
+
const withoutProtocol = filePath.substring(filePath.indexOf("://") + 3);
|
|
226
|
+
filePath = withoutProtocol;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (bucket && filePath && !filePath.startsWith(bucket)) {
|
|
230
|
+
filePath = `${bucket}/${filePath}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!filePath || filePath.trim() === "" || filePath === "/") {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const response = await fetchWithAuth(`${storageBasePath}/file/${filePath}`, {
|
|
238
|
+
method: "DELETE"
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (!response.ok && response.status !== 404) {
|
|
242
|
+
const error = await response.json().catch(() => ({ error: "Failed to delete file" }));
|
|
243
|
+
throw new Error(error.error || "Failed to delete file");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Clear from cache
|
|
247
|
+
urlsCache.delete(bucket ? `${bucket}/${key}` : key);
|
|
248
|
+
}, [fetchWithAuth, storageBasePath, urlsCache]);
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* List files in a path
|
|
252
|
+
*/
|
|
253
|
+
const listObjects = useCallback(async (
|
|
254
|
+
prefix: string,
|
|
255
|
+
options?: {
|
|
256
|
+
bucket?: string;
|
|
257
|
+
maxResults?: number;
|
|
258
|
+
pageToken?: string;
|
|
259
|
+
}
|
|
260
|
+
): Promise<StorageListResult> => {
|
|
261
|
+
const params = new URLSearchParams();
|
|
262
|
+
if (prefix) params.set("prefix", prefix);
|
|
263
|
+
if (options?.bucket) params.set("bucket", options.bucket);
|
|
264
|
+
if (options?.maxResults) params.set("maxResults", String(options.maxResults));
|
|
265
|
+
if (options?.pageToken) params.set("pageToken", options.pageToken);
|
|
266
|
+
|
|
267
|
+
const response = await fetchWithAuth(
|
|
268
|
+
`${storageBasePath}/list?${params.toString()}`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
const error = await response.json().catch(() => ({ error: "Failed to list files" }));
|
|
273
|
+
throw new Error(error.error || "Failed to list files");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const result = await response.json();
|
|
277
|
+
return result.data;
|
|
278
|
+
}, [fetchWithAuth, storageBasePath]);
|
|
279
|
+
|
|
280
|
+
// Return memoized StorageSource
|
|
281
|
+
return useMemo<StorageSource>(() => ({
|
|
282
|
+
putObject,
|
|
283
|
+
getSignedUrl,
|
|
284
|
+
getObject,
|
|
285
|
+
deleteObject,
|
|
286
|
+
listObjects
|
|
287
|
+
}), [putObject, getSignedUrl, getObject, deleteObject, listObjects]);
|
|
288
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useContext, useEffect } from "react";
|
|
2
|
+
import type { StudioBridge } from "./useStudioBridge";
|
|
3
|
+
import { StudioBridgeRegistryContext } from "./useStudioBridge";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registers a value into the self-assembling Studio bridge.
|
|
7
|
+
*
|
|
8
|
+
* Each controller (collectionRegistry, sidePanel, url, navigation, breadcrumbs)
|
|
9
|
+
* calls this hook on mount to inject its real implementation into the bridge.
|
|
10
|
+
* On unmount the slice is automatically removed, reverting to the noop default.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```tsx
|
|
14
|
+
* function SomeProvider({ children }) {
|
|
15
|
+
* const controller = useBuildSomeController();
|
|
16
|
+
* useBridgeRegistration("sidePanelController", controller);
|
|
17
|
+
* return <SomeContext.Provider value={controller}>{children}</SomeContext.Provider>;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useBridgeRegistration<K extends keyof StudioBridge>(
|
|
22
|
+
key: K,
|
|
23
|
+
value: StudioBridge[K]
|
|
24
|
+
): void {
|
|
25
|
+
const registry = useContext(StudioBridgeRegistryContext);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!registry) return;
|
|
29
|
+
registry.register(key, value);
|
|
30
|
+
return () => registry.unregister(key);
|
|
31
|
+
}, [registry, key, value]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
const rebaseLogo = "data:image/png;base64,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";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal hook to handle the browser title and icon
|
|
7
|
+
* @param name
|
|
8
|
+
* @param logo
|
|
9
|
+
*/
|
|
10
|
+
export function useBrowserTitleAndIcon(name: string, logo?: string) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (document) {
|
|
13
|
+
document.title = `${name} - Rebase`;
|
|
14
|
+
let link = document.querySelector("link[rel~='icon']") as HTMLLinkElement | null;
|
|
15
|
+
if (!link) {
|
|
16
|
+
link = document.createElement("link");
|
|
17
|
+
link.rel = "icon";
|
|
18
|
+
document.getElementsByTagName("head")[0].appendChild(link);
|
|
19
|
+
}
|
|
20
|
+
link.href = logo ?? rebaseLogo;
|
|
21
|
+
}
|
|
22
|
+
}, [name, logo]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { AdminModeController } from "./index";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Use this hook to build an admin mode controller that determines
|
|
6
|
+
* whether the UI shows Developer or Editor tools.
|
|
7
|
+
*/
|
|
8
|
+
export function useBuildAdminModeController(): AdminModeController {
|
|
9
|
+
|
|
10
|
+
const savedMode = typeof window !== "undefined" ? localStorage.getItem("rebase-admin-mode") as "content" | "studio" | "settings" | null : null;
|
|
11
|
+
const [mode, setMode] = useState<"content" | "studio" | "settings">(savedMode ?? "content");
|
|
12
|
+
|
|
13
|
+
const setModeInternal = useCallback((newMode: "content" | "studio" | "settings") => {
|
|
14
|
+
if (typeof window !== "undefined") {
|
|
15
|
+
localStorage.setItem("rebase-admin-mode", newMode);
|
|
16
|
+
}
|
|
17
|
+
setMode(newMode);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return useMemo(() => ({
|
|
21
|
+
mode,
|
|
22
|
+
setMode: setModeInternal
|
|
23
|
+
}), [mode, setModeInternal]);
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { EffectiveRoleController } from "@rebasepro/types";
|
|
2
|
+
import { useCallback, useState, useMemo } from "react";
|
|
3
|
+
;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Use this hook to build an effective role controller that determines
|
|
7
|
+
* what role is simulated in Editor mode when Dev mode is active.
|
|
8
|
+
*
|
|
9
|
+
* It uses localStorage to persist the simulated role across reloads.
|
|
10
|
+
*/
|
|
11
|
+
export function useBuildEffectiveRoleController(): EffectiveRoleController {
|
|
12
|
+
|
|
13
|
+
const savedRole = typeof window !== "undefined" ? localStorage.getItem("rebase-effective-role") : null;
|
|
14
|
+
const [effectiveRole, setEffectiveRole] = useState<string | null>(savedRole ?? null);
|
|
15
|
+
|
|
16
|
+
const setRoleInternal = useCallback((newRole: string | null) => {
|
|
17
|
+
if (typeof window !== "undefined") {
|
|
18
|
+
if (newRole) {
|
|
19
|
+
localStorage.setItem("rebase-effective-role", newRole);
|
|
20
|
+
} else {
|
|
21
|
+
localStorage.removeItem("rebase-effective-role");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
setEffectiveRole(newRole);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return useMemo(() => ({
|
|
28
|
+
effectiveRole,
|
|
29
|
+
setEffectiveRole: setRoleInternal
|
|
30
|
+
}), [effectiveRole, setRoleInternal]);
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
|
2
|
+
import { PartialCollectionConfig, UserConfigurationPersistence } from "@rebasepro/types";
|
|
3
|
+
import { stripCollectionPath } from "@rebasepro/common";
|
|
4
|
+
import { mergeDeep } from "@rebasepro/utils";
|
|
5
|
+
|
|
6
|
+
export function useBuildLocalConfigurationPersistence(): UserConfigurationPersistence {
|
|
7
|
+
|
|
8
|
+
const configCache = useRef<Record<string, PartialCollectionConfig>>({});
|
|
9
|
+
|
|
10
|
+
const getCollectionFromStorage = useCallback((storageKey: string) => {
|
|
11
|
+
const item = localStorage.getItem(storageKey);
|
|
12
|
+
return item ? JSON.parse(item) : {};
|
|
13
|
+
}, []);
|
|
14
|
+
|
|
15
|
+
const getCollectionConfig = useCallback(<M extends Record<string, any>>(path: string): PartialCollectionConfig<M> => {
|
|
16
|
+
const storageKey = `collection_config::${stripCollectionPath(path)}`;
|
|
17
|
+
if (configCache.current[storageKey]) {
|
|
18
|
+
return configCache.current[storageKey] as PartialCollectionConfig<M>;
|
|
19
|
+
}
|
|
20
|
+
return getCollectionFromStorage(storageKey);
|
|
21
|
+
}, [getCollectionFromStorage]);
|
|
22
|
+
|
|
23
|
+
const onCollectionModified = useCallback(<M extends Record<string, any>>(path: string, data: PartialCollectionConfig<M>) => {
|
|
24
|
+
const storageKey = `collection_config::${stripCollectionPath(path)}`;
|
|
25
|
+
localStorage.setItem(storageKey, JSON.stringify(data));
|
|
26
|
+
const cachedConfig = configCache.current[storageKey];
|
|
27
|
+
const newConfig = mergeDeep(cachedConfig ?? getCollectionFromStorage(path), data);
|
|
28
|
+
configCache.current[storageKey] = mergeDeep(configCache.current[storageKey], newConfig);
|
|
29
|
+
}, [getCollectionFromStorage]);
|
|
30
|
+
|
|
31
|
+
const [recentlyVisitedPaths, _setRecentlyVisitedPaths] = useState<string[]>([]);
|
|
32
|
+
const [favouritePaths, _setFavouritePaths] = useState<string[]>([]);
|
|
33
|
+
const [collapsedGroups, _setCollapsedGroups] = useState<string[]>([]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
_setRecentlyVisitedPaths(localStorage.getItem("recently_visited_paths") ? JSON.parse(localStorage.getItem("recently_visited_paths")!) : []);
|
|
37
|
+
_setFavouritePaths(localStorage.getItem("favourite_paths") ? JSON.parse(localStorage.getItem("favourite_paths")!) : []);
|
|
38
|
+
_setCollapsedGroups(localStorage.getItem("collapsed_groups") ? JSON.parse(localStorage.getItem("collapsed_groups")!) : []);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
const setRecentlyVisitedPaths = useCallback((paths: string[]) => {
|
|
42
|
+
localStorage.setItem("recently_visited_paths", JSON.stringify(paths));
|
|
43
|
+
_setRecentlyVisitedPaths(paths);
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
const setFavouritePaths = useCallback((paths: string[]) => {
|
|
47
|
+
localStorage.setItem("favourite_paths", JSON.stringify(paths));
|
|
48
|
+
_setFavouritePaths(paths);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const setCollapsedGroups = useCallback((paths: string[]) => {
|
|
52
|
+
localStorage.setItem("collapsed_groups", JSON.stringify(paths));
|
|
53
|
+
_setCollapsedGroups(paths);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
return useMemo(() => ({
|
|
57
|
+
onCollectionModified,
|
|
58
|
+
getCollectionConfig,
|
|
59
|
+
recentlyVisitedPaths,
|
|
60
|
+
setRecentlyVisitedPaths,
|
|
61
|
+
favouritePaths,
|
|
62
|
+
setFavouritePaths,
|
|
63
|
+
collapsedGroups,
|
|
64
|
+
setCollapsedGroups
|
|
65
|
+
}), [
|
|
66
|
+
onCollectionModified,
|
|
67
|
+
getCollectionConfig,
|
|
68
|
+
recentlyVisitedPaths,
|
|
69
|
+
setRecentlyVisitedPaths,
|
|
70
|
+
favouritePaths,
|
|
71
|
+
setFavouritePaths,
|
|
72
|
+
collapsedGroups,
|
|
73
|
+
setCollapsedGroups
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState, useMemo } from "react";
|
|
2
|
+
|
|
3
|
+
import { ModeController } from "./index";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Use this hook to build a color mode controller that determines
|
|
7
|
+
* the theme of the CMS
|
|
8
|
+
*/
|
|
9
|
+
export function useBuildModeController(): ModeController {
|
|
10
|
+
|
|
11
|
+
const prefersDarkModeQuery = useCallback((): boolean => {
|
|
12
|
+
if (typeof window === "undefined")
|
|
13
|
+
return false;
|
|
14
|
+
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
|
|
15
|
+
return mediaQueryList.matches;
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const [mode, setMode] = useState<"light" | "dark">(() => {
|
|
19
|
+
const prefersDarkModeStorage = typeof window !== "undefined" && localStorage.getItem("prefers-dark-mode") != null
|
|
20
|
+
? localStorage.getItem("prefers-dark-mode") === "true"
|
|
21
|
+
: null;
|
|
22
|
+
const prefersDarkMode = prefersDarkModeStorage ?? prefersDarkModeQuery();
|
|
23
|
+
return prefersDarkMode ? "dark" : "light";
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
28
|
+
const handleChange = (e: MediaQueryListEvent) => {
|
|
29
|
+
if (localStorage.getItem("prefers-dark-mode") == null) {
|
|
30
|
+
setMode(e.matches ? "dark" : "light");
|
|
31
|
+
setDocumentMode(e.matches ? "dark" : "light");
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Initial setup
|
|
36
|
+
setDocumentMode(mode);
|
|
37
|
+
|
|
38
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
39
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
const setDocumentMode = (mode: "light" | "dark") => {
|
|
43
|
+
if (mode === "dark") {
|
|
44
|
+
document.documentElement.classList.add("dark");
|
|
45
|
+
} else {
|
|
46
|
+
document.documentElement.classList.remove("dark");
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const setModeInternal = useCallback((mode: "light" | "dark" | "system") => {
|
|
51
|
+
if (mode === "light") {
|
|
52
|
+
setDocumentMode("light");
|
|
53
|
+
localStorage.setItem("prefers-dark-mode", "false");
|
|
54
|
+
setMode("light");
|
|
55
|
+
} else if (mode === "dark") {
|
|
56
|
+
setDocumentMode("dark");
|
|
57
|
+
localStorage.setItem("prefers-dark-mode", "true");
|
|
58
|
+
setMode("dark");
|
|
59
|
+
} else {
|
|
60
|
+
const preferredMode = prefersDarkModeQuery() ? "dark" : "light";
|
|
61
|
+
setDocumentMode(preferredMode);
|
|
62
|
+
localStorage.removeItem("prefers-dark-mode");
|
|
63
|
+
setMode(preferredMode);
|
|
64
|
+
}
|
|
65
|
+
}, [prefersDarkModeQuery]);
|
|
66
|
+
|
|
67
|
+
return useMemo(() => ({
|
|
68
|
+
mode,
|
|
69
|
+
setMode: setModeInternal
|
|
70
|
+
}), [mode, setModeInternal]);
|
|
71
|
+
}
|