-
-
Save shricodev/5403f82ea5cf5991c14bc43ce3f47476 to your computer and use it in GitHub Desktop.
Test 1: Add a global Action Palette (Ctrl + K) - claude-opus-4.5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| diff --git a/opus-4.5.patch b/opus-4.5.patch | |
| new file mode 100644 | |
| index 0000000..9dfd197 | |
| --- /dev/null | |
| +++ b/opus-4.5.patch | |
| @@ -0,0 +1,72 @@ | |
| +diff --git a/src/components/App.tsx b/src/components/App.tsx | |
| +index a578cc8..958747d 100644 | |
| +--- a/src/components/App.tsx | |
| ++++ b/src/components/App.tsx | |
| +@@ -1,7 +1,7 @@ | |
| + import { BrowserRouter, useRoutes } from 'react-router-dom'; | |
| + import routesConfig from '../config/routesConfig'; | |
| + import Navbar from './Navbar'; | |
| +-import { Suspense, useState, useEffect } from 'react'; | |
| ++import { Suspense, useState, useEffect, useCallback } from 'react'; | |
| + import Loading from './Loading'; | |
| + import { CssBaseline, Theme, ThemeProvider } from '@mui/material'; | |
| + import { CustomSnackBarProvider } from '../contexts/CustomSnackBarContext'; | |
| +@@ -13,6 +13,7 @@ import ScrollToTopButton from './ScrollToTopButton'; | |
| + import { I18nextProvider } from 'react-i18next'; | |
| + import i18n from '../i18n'; | |
| + import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider'; | |
| ++import ActionPalette from './ActionPalette'; | |
| + | |
| + export type Mode = 'dark' | 'light' | 'system'; | |
| + | |
| +@@ -29,8 +30,28 @@ function App() { | |
| + () => (localStorage.getItem('theme') || 'system') as Mode | |
| + ); | |
| + const [theme, setTheme] = useState<Theme>(() => getTheme(mode)); | |
| ++ const [actionPaletteOpen, setActionPaletteOpen] = useState(false); | |
| ++ | |
| + useEffect(() => setTheme(getTheme(mode)), [mode]); | |
| + | |
| ++ // Global keyboard shortcut handler for Ctrl/Cmd + K | |
| ++ useEffect(() => { | |
| ++ const handleKeyDown = (e: KeyboardEvent) => { | |
| ++ if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| ++ e.preventDefault(); | |
| ++ setActionPaletteOpen(true); | |
| ++ } | |
| ++ }; | |
| ++ | |
| ++ window.addEventListener('keydown', handleKeyDown); | |
| ++ return () => window.removeEventListener('keydown', handleKeyDown); | |
| ++ }, []); | |
| ++ | |
| ++ const handleChangeMode = useCallback(() => { | |
| ++ setMode((prev) => nextMode(prev)); | |
| ++ localStorage.setItem('theme', nextMode(mode)); | |
| ++ }, [mode]); | |
| ++ | |
| + // Make sure to update the theme when the mode changes | |
| + useEffect(() => { | |
| + const systemDarkModeQuery = window.matchMedia( | |
| +@@ -62,14 +83,17 @@ function App() { | |
| + <BrowserRouter> | |
| + <Navbar | |
| + mode={mode} | |
| +- onChangeMode={() => { | |
| +- setMode((prev) => nextMode(prev)); | |
| +- localStorage.setItem('theme', nextMode(mode)); | |
| +- }} | |
| ++ onChangeMode={handleChangeMode} | |
| + /> | |
| + <Suspense fallback={<Loading />}> | |
| + <AppRoutes /> | |
| + </Suspense> | |
| ++ <ActionPalette | |
| ++ open={actionPaletteOpen} | |
| ++ onClose={() => setActionPaletteOpen(false)} | |
| ++ mode={mode} | |
| ++ onChangeMode={handleChangeMode} | |
| ++ /> | |
| + </BrowserRouter> | |
| + </UserTypeFilterProvider> | |
| + </CustomSnackBarProvider> | |
| diff --git a/src/components/ActionPalette/ActionPalette.tsx b/src/components/ActionPalette/ActionPalette.tsx | |
| new file mode 100644 | |
| index 0000000..eacdc72 | |
| --- /dev/null | |
| +++ b/src/components/ActionPalette/ActionPalette.tsx | |
| @@ -0,0 +1,528 @@ | |
| +import React, { useState, useEffect, useMemo, useCallback } from 'react'; | |
| +import { | |
| + Dialog, | |
| + DialogContent, | |
| + TextField, | |
| + List, | |
| + ListItem, | |
| + ListItemButton, | |
| + ListItemText, | |
| + ListItemIcon, | |
| + Typography, | |
| + Box, | |
| + Stack, | |
| + useTheme, | |
| + Chip, | |
| + Paper, | |
| + InputAdornment | |
| +} from '@mui/material'; | |
| +import { Icon } from '@iconify/react'; | |
| +import { useNavigate } from 'react-router-dom'; | |
| +import { useTranslation } from 'react-i18next'; | |
| +import { tools } from '@tools/index'; | |
| +import { DefinedTool } from '@tools/defineTool'; | |
| +import { useUserTypeFilter } from 'providers/UserTypeFilterProvider'; | |
| +import { filterTools } from '@tools/index'; | |
| +import { | |
| + getBookmarkedToolPaths, | |
| + toggleBookmarked as toggleBookmark | |
| +} from '@utils/bookmark'; | |
| +import { validNamespaces } from '../../i18n'; | |
| + | |
| +type ActionType = 'tool' | 'action'; | |
| + | |
| +interface Action { | |
| + id: string; | |
| + type: ActionType; | |
| + label: string; | |
| + description?: string; | |
| + icon: string; | |
| + category?: string; | |
| + keywords?: string[]; | |
| + action?: () => void; | |
| + tool?: DefinedTool; | |
| +} | |
| + | |
| +interface ActionPaletteProps { | |
| + open: boolean; | |
| + onClose: () => void; | |
| + mode: 'dark' | 'light' | 'system'; | |
| + onChangeMode: () => void; | |
| +} | |
| + | |
| +const ActionPalette: React.FC<ActionPaletteProps> = ({ | |
| + open, | |
| + onClose, | |
| + mode, | |
| + onChangeMode | |
| +}) => { | |
| + const { t, i18n } = useTranslation(validNamespaces); | |
| + const navigate = useNavigate(); | |
| + const theme = useTheme(); | |
| + const { selectedUserTypes, setSelectedUserTypes } = useUserTypeFilter(); | |
| + const [searchQuery, setSearchQuery] = useState(''); | |
| + const [selectedIndex, setSelectedIndex] = useState(0); | |
| + const [bookmarkedPaths, setBookmarkedPaths] = useState<string[]>([]); | |
| + | |
| + // Load bookmarked paths | |
| + useEffect(() => { | |
| + if (open) { | |
| + setBookmarkedPaths(getBookmarkedToolPaths()); | |
| + } | |
| + }, [open]); | |
| + | |
| + // Reset state when opening | |
| + useEffect(() => { | |
| + if (open) { | |
| + setSearchQuery(''); | |
| + setSelectedIndex(0); | |
| + } | |
| + }, [open]); | |
| + | |
| + const getModeIcon = () => { | |
| + switch (mode) { | |
| + case 'dark': | |
| + return 'mdi:weather-night'; | |
| + case 'light': | |
| + return 'mdi:weather-sunny'; | |
| + default: | |
| + return 'mdi:laptop'; | |
| + } | |
| + }; | |
| + | |
| + const getModeLabel = () => { | |
| + switch (mode) { | |
| + case 'dark': | |
| + return 'Dark Mode'; | |
| + case 'light': | |
| + return 'Light Mode'; | |
| + default: | |
| + return 'System Mode'; | |
| + } | |
| + }; | |
| + | |
| + // Build all available actions | |
| + const allActions = useMemo(() => { | |
| + const actions: Action[] = []; | |
| + | |
| + // Add tool navigation actions | |
| + const filteredTools = filterTools(tools, '', selectedUserTypes, t); | |
| + filteredTools.forEach((tool) => { | |
| + actions.push({ | |
| + id: `tool-${tool.path}`, | |
| + type: 'tool', | |
| + label: t(tool.name), | |
| + description: t(tool.shortDescription), | |
| + icon: typeof tool.icon === 'string' ? tool.icon : 'mdi:tools', | |
| + category: 'Tools', | |
| + keywords: tool.keywords, | |
| + tool | |
| + }); | |
| + }); | |
| + | |
| + // Add system actions | |
| + actions.push({ | |
| + id: 'toggle-theme', | |
| + type: 'action', | |
| + label: `Toggle Theme (Current: ${getModeLabel()})`, | |
| + description: 'Switch between light, dark, and system theme', | |
| + icon: getModeIcon(), | |
| + category: 'Settings', | |
| + keywords: ['theme', 'dark', 'light', 'mode', 'appearance'], | |
| + action: onChangeMode | |
| + }); | |
| + | |
| + // Add language switch actions | |
| + const languages = [ | |
| + { code: 'en', label: 'English' }, | |
| + { code: 'de', label: 'Deutsch' }, | |
| + { code: 'es', label: 'Español' }, | |
| + { code: 'fr', label: 'Français' }, | |
| + { code: 'pt', label: 'Português' }, | |
| + { code: 'ja', label: '日本語' }, | |
| + { code: 'hi', label: 'हिंदी' }, | |
| + { code: 'nl', label: 'Nederlands' }, | |
| + { code: 'ru', label: 'Русский' }, | |
| + { code: 'zh', label: '中文' } | |
| + ]; | |
| + | |
| + languages.forEach((lang) => { | |
| + actions.push({ | |
| + id: `lang-${lang.code}`, | |
| + type: 'action', | |
| + label: `Switch to ${lang.label}`, | |
| + description: | |
| + i18n.language === lang.code ? 'Currently active' : undefined, | |
| + icon: 'mdi:translate', | |
| + category: 'Language', | |
| + keywords: [ | |
| + 'language', | |
| + 'translate', | |
| + lang.label.toLowerCase(), | |
| + lang.code | |
| + ], | |
| + action: () => { | |
| + i18n.changeLanguage(lang.code); | |
| + localStorage.setItem('lang', lang.code); | |
| + } | |
| + }); | |
| + }); | |
| + | |
| + // User type filter actions | |
| + actions.push({ | |
| + id: 'toggle-general-user', | |
| + type: 'action', | |
| + label: `${ | |
| + selectedUserTypes.includes('generalUsers') ? 'Hide' : 'Show' | |
| + } General User Tools`, | |
| + description: 'Toggle visibility of general user tools', | |
| + icon: 'mdi:account', | |
| + category: 'Filters', | |
| + keywords: ['filter', 'user', 'general', 'toggle'], | |
| + action: () => { | |
| + if (selectedUserTypes.includes('generalUsers')) { | |
| + setSelectedUserTypes( | |
| + selectedUserTypes.filter((t) => t !== 'generalUsers') | |
| + ); | |
| + } else { | |
| + setSelectedUserTypes([...selectedUserTypes, 'generalUsers']); | |
| + } | |
| + } | |
| + }); | |
| + | |
| + actions.push({ | |
| + id: 'toggle-developer', | |
| + type: 'action', | |
| + label: `${ | |
| + selectedUserTypes.includes('developers') ? 'Hide' : 'Show' | |
| + } Developer Tools`, | |
| + description: 'Toggle visibility of developer tools', | |
| + icon: 'mdi:code-braces', | |
| + category: 'Filters', | |
| + keywords: ['filter', 'developer', 'dev', 'toggle'], | |
| + action: () => { | |
| + if (selectedUserTypes.includes('developers')) { | |
| + setSelectedUserTypes( | |
| + selectedUserTypes.filter((t) => t !== 'developers') | |
| + ); | |
| + } else { | |
| + setSelectedUserTypes([...selectedUserTypes, 'developers']); | |
| + } | |
| + } | |
| + }); | |
| + | |
| + // Navigation actions | |
| + actions.push({ | |
| + id: 'go-home', | |
| + type: 'action', | |
| + label: 'Go to Home', | |
| + description: 'Navigate to the home page', | |
| + icon: 'mdi:home', | |
| + category: 'Navigation', | |
| + keywords: ['home', 'main', 'start', 'navigate'], | |
| + action: () => { | |
| + navigate('/'); | |
| + onClose(); | |
| + } | |
| + }); | |
| + | |
| + // Bookmarks action | |
| + if (bookmarkedPaths.length > 0) { | |
| + actions.push({ | |
| + id: 'view-bookmarks', | |
| + type: 'action', | |
| + label: 'View Bookmarked Tools', | |
| + description: `You have ${bookmarkedPaths.length} bookmarked tool${ | |
| + bookmarkedPaths.length > 1 ? 's' : '' | |
| + }`, | |
| + icon: 'mdi:bookmark', | |
| + category: 'Navigation', | |
| + keywords: ['bookmark', 'favorites', 'saved'], | |
| + action: () => { | |
| + navigate('/'); | |
| + onClose(); | |
| + } | |
| + }); | |
| + | |
| + actions.push({ | |
| + id: 'clear-bookmarks', | |
| + type: 'action', | |
| + label: 'Clear All Bookmarks', | |
| + description: 'Remove all bookmarked tools', | |
| + icon: 'mdi:bookmark-remove', | |
| + category: 'Navigation', | |
| + keywords: ['bookmark', 'clear', 'remove', 'delete'], | |
| + action: () => { | |
| + localStorage.removeItem('bookmarkedTools'); | |
| + setBookmarkedPaths([]); | |
| + } | |
| + }); | |
| + } | |
| + | |
| + return actions; | |
| + }, [ | |
| + selectedUserTypes, | |
| + t, | |
| + i18n, | |
| + mode, | |
| + navigate, | |
| + onChangeMode, | |
| + setSelectedUserTypes, | |
| + bookmarkedPaths, | |
| + onClose | |
| + ]); | |
| + | |
| + // Filter actions based on search query | |
| + const filteredActions = useMemo(() => { | |
| + if (!searchQuery.trim()) { | |
| + return allActions; | |
| + } | |
| + | |
| + const query = searchQuery.toLowerCase(); | |
| + return allActions.filter((action) => { | |
| + // Check label | |
| + if (action.label.toLowerCase().includes(query)) return true; | |
| + // Check description | |
| + if (action.description?.toLowerCase().includes(query)) return true; | |
| + // Check category | |
| + if (action.category?.toLowerCase().includes(query)) return true; | |
| + // Check keywords | |
| + if (action.keywords?.some((k) => k.toLowerCase().includes(query))) | |
| + return true; | |
| + return false; | |
| + }); | |
| + }, [allActions, searchQuery]); | |
| + | |
| + // Group actions by category | |
| + const groupedActions = useMemo(() => { | |
| + const groups: Record<string, Action[]> = {}; | |
| + filteredActions.forEach((action) => { | |
| + const category = action.category || 'Other'; | |
| + if (!groups[category]) { | |
| + groups[category] = []; | |
| + } | |
| + groups[category].push(action); | |
| + }); | |
| + return groups; | |
| + }, [filteredActions]); | |
| + | |
| + // Flatten actions for keyboard navigation | |
| + const flatActions = useMemo(() => { | |
| + return Object.values(groupedActions).flat(); | |
| + }, [groupedActions]); | |
| + | |
| + // Keyboard navigation | |
| + useEffect(() => { | |
| + const handleKeyDown = (e: KeyboardEvent) => { | |
| + if (!open) return; | |
| + | |
| + switch (e.key) { | |
| + case 'ArrowDown': | |
| + e.preventDefault(); | |
| + setSelectedIndex((prev) => | |
| + prev < flatActions.length - 1 ? prev + 1 : 0 | |
| + ); | |
| + break; | |
| + case 'ArrowUp': | |
| + e.preventDefault(); | |
| + setSelectedIndex((prev) => | |
| + prev > 0 ? prev - 1 : flatActions.length - 1 | |
| + ); | |
| + break; | |
| + case 'Enter': | |
| + e.preventDefault(); | |
| + if (flatActions[selectedIndex]) { | |
| + executeAction(flatActions[selectedIndex]); | |
| + } | |
| + break; | |
| + case 'Escape': | |
| + e.preventDefault(); | |
| + onClose(); | |
| + break; | |
| + } | |
| + }; | |
| + | |
| + window.addEventListener('keydown', handleKeyDown); | |
| + return () => window.removeEventListener('keydown', handleKeyDown); | |
| + }, [open, selectedIndex, flatActions, onClose]); | |
| + | |
| + const executeAction = useCallback( | |
| + (action: Action) => { | |
| + if (action.type === 'tool' && action.tool) { | |
| + navigate('/' + action.tool.path); | |
| + onClose(); | |
| + } else if (action.type === 'action' && action.action) { | |
| + action.action(); | |
| + // Only close for navigation actions | |
| + if (action.id.startsWith('go-') || action.id === 'view-bookmarks') { | |
| + onClose(); | |
| + } | |
| + } | |
| + }, | |
| + [navigate, onClose] | |
| + ); | |
| + | |
| + // Reset selected index when search changes | |
| + useEffect(() => { | |
| + setSelectedIndex(0); | |
| + }, [searchQuery]); | |
| + | |
| + // Scroll selected item into view | |
| + useEffect(() => { | |
| + if (open) { | |
| + const element = document.getElementById(`action-item-${selectedIndex}`); | |
| + element?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
| + } | |
| + }, [selectedIndex, open]); | |
| + | |
| + return ( | |
| + <Dialog | |
| + open={open} | |
| + onClose={onClose} | |
| + maxWidth="md" | |
| + fullWidth | |
| + PaperProps={{ | |
| + sx: { | |
| + position: 'fixed', | |
| + top: '20%', | |
| + transform: 'translateY(-20%)', | |
| + maxHeight: '60vh', | |
| + borderRadius: 2 | |
| + } | |
| + }} | |
| + > | |
| + <DialogContent sx={{ p: 0, display: 'flex', flexDirection: 'column' }}> | |
| + <Box sx={{ p: 2, borderBottom: 1, borderColor: 'divider' }}> | |
| + <TextField | |
| + fullWidth | |
| + autoFocus | |
| + placeholder="Search for tools and actions..." | |
| + value={searchQuery} | |
| + onChange={(e) => setSearchQuery(e.target.value)} | |
| + InputProps={{ | |
| + startAdornment: ( | |
| + <InputAdornment position="start"> | |
| + <Icon icon="mdi:magnify" /> | |
| + </InputAdornment> | |
| + ), | |
| + endAdornment: ( | |
| + <InputAdornment position="end"> | |
| + <Stack direction="row" spacing={0.5}> | |
| + <Chip | |
| + size="small" | |
| + label="↑↓" | |
| + variant="outlined" | |
| + sx={{ fontSize: '0.7rem' }} | |
| + /> | |
| + <Chip | |
| + size="small" | |
| + label="Enter" | |
| + variant="outlined" | |
| + sx={{ fontSize: '0.7rem' }} | |
| + /> | |
| + <Chip | |
| + size="small" | |
| + label="Esc" | |
| + variant="outlined" | |
| + sx={{ fontSize: '0.7rem' }} | |
| + /> | |
| + </Stack> | |
| + </InputAdornment> | |
| + ), | |
| + sx: { | |
| + '& .MuiOutlinedInput-notchedOutline': { border: 'none' } | |
| + } | |
| + }} | |
| + /> | |
| + </Box> | |
| + | |
| + <Box sx={{ flexGrow: 1, overflowY: 'auto' }}> | |
| + {Object.entries(groupedActions).length === 0 ? ( | |
| + <Box sx={{ p: 3, textAlign: 'center' }}> | |
| + <Typography variant="body2" color="text.secondary"> | |
| + No results found for "{searchQuery}" | |
| + </Typography> | |
| + </Box> | |
| + ) : ( | |
| + <List sx={{ py: 0 }}> | |
| + {Object.entries(groupedActions).map( | |
| + ([category, actions], categoryIndex) => ( | |
| + <React.Fragment key={category}> | |
| + <Box | |
| + sx={{ | |
| + px: 2, | |
| + py: 0.5, | |
| + backgroundColor: 'action.hover', | |
| + position: 'sticky', | |
| + top: 0, | |
| + zIndex: 1 | |
| + }} | |
| + > | |
| + <Typography | |
| + variant="caption" | |
| + color="text.secondary" | |
| + fontWeight="bold" | |
| + > | |
| + {category.toUpperCase()} | |
| + </Typography> | |
| + </Box> | |
| + {actions.map((action, actionIndex) => { | |
| + const globalIndex = | |
| + Object.values(groupedActions) | |
| + .slice(0, categoryIndex) | |
| + .reduce((acc, arr) => acc + arr.length, 0) + | |
| + actionIndex; | |
| + | |
| + return ( | |
| + <ListItem | |
| + key={action.id} | |
| + id={`action-item-${globalIndex}`} | |
| + disablePadding | |
| + sx={{ | |
| + backgroundColor: | |
| + selectedIndex === globalIndex | |
| + ? 'action.selected' | |
| + : 'transparent' | |
| + }} | |
| + > | |
| + <ListItemButton | |
| + onClick={() => executeAction(action)} | |
| + onMouseEnter={() => setSelectedIndex(globalIndex)} | |
| + > | |
| + <ListItemIcon sx={{ minWidth: 36 }}> | |
| + <Icon icon={action.icon} fontSize={20} /> | |
| + </ListItemIcon> | |
| + <ListItemText | |
| + primary={action.label} | |
| + secondary={action.description} | |
| + primaryTypographyProps={{ | |
| + fontSize: '0.9rem' | |
| + }} | |
| + secondaryTypographyProps={{ | |
| + fontSize: '0.75rem' | |
| + }} | |
| + /> | |
| + {action.type === 'tool' && ( | |
| + <Chip | |
| + size="small" | |
| + label="Tool" | |
| + variant="outlined" | |
| + sx={{ ml: 1 }} | |
| + /> | |
| + )} | |
| + </ListItemButton> | |
| + </ListItem> | |
| + ); | |
| + })} | |
| + </React.Fragment> | |
| + ) | |
| + )} | |
| + </List> | |
| + )} | |
| + </Box> | |
| + </DialogContent> | |
| + </Dialog> | |
| + ); | |
| +}; | |
| + | |
| +export default ActionPalette; | |
| diff --git a/src/components/ActionPalette/index.ts b/src/components/ActionPalette/index.ts | |
| new file mode 100644 | |
| index 0000000..ca94a8a | |
| --- /dev/null | |
| +++ b/src/components/ActionPalette/index.ts | |
| @@ -0,0 +1 @@ | |
| +export { default } from './ActionPalette'; | |
| diff --git a/src/components/App.tsx b/src/components/App.tsx | |
| index a578cc8..889442d 100644 | |
| --- a/src/components/App.tsx | |
| +++ b/src/components/App.tsx | |
| @@ -1,7 +1,7 @@ | |
| import { BrowserRouter, useRoutes } from 'react-router-dom'; | |
| import routesConfig from '../config/routesConfig'; | |
| import Navbar from './Navbar'; | |
| -import { Suspense, useState, useEffect } from 'react'; | |
| +import { Suspense, useState, useEffect, useCallback } from 'react'; | |
| import Loading from './Loading'; | |
| import { CssBaseline, Theme, ThemeProvider } from '@mui/material'; | |
| import { CustomSnackBarProvider } from '../contexts/CustomSnackBarContext'; | |
| @@ -13,6 +13,7 @@ import ScrollToTopButton from './ScrollToTopButton'; | |
| import { I18nextProvider } from 'react-i18next'; | |
| import i18n from '../i18n'; | |
| import { UserTypeFilterProvider } from 'providers/UserTypeFilterProvider'; | |
| +import ActionPalette from './ActionPalette'; | |
| export type Mode = 'dark' | 'light' | 'system'; | |
| @@ -29,8 +30,28 @@ function App() { | |
| () => (localStorage.getItem('theme') || 'system') as Mode | |
| ); | |
| const [theme, setTheme] = useState<Theme>(() => getTheme(mode)); | |
| + const [actionPaletteOpen, setActionPaletteOpen] = useState(false); | |
| + | |
| useEffect(() => setTheme(getTheme(mode)), [mode]); | |
| + // Global keyboard shortcut handler for Ctrl/Cmd + K | |
| + useEffect(() => { | |
| + const handleKeyDown = (e: KeyboardEvent) => { | |
| + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| + e.preventDefault(); | |
| + setActionPaletteOpen(true); | |
| + } | |
| + }; | |
| + | |
| + window.addEventListener('keydown', handleKeyDown); | |
| + return () => window.removeEventListener('keydown', handleKeyDown); | |
| + }, []); | |
| + | |
| + const handleChangeMode = useCallback(() => { | |
| + setMode((prev) => nextMode(prev)); | |
| + localStorage.setItem('theme', nextMode(mode)); | |
| + }, [mode]); | |
| + | |
| // Make sure to update the theme when the mode changes | |
| useEffect(() => { | |
| const systemDarkModeQuery = window.matchMedia( | |
| @@ -60,16 +81,16 @@ function App() { | |
| <CustomSnackBarProvider> | |
| <UserTypeFilterProvider> | |
| <BrowserRouter> | |
| - <Navbar | |
| - mode={mode} | |
| - onChangeMode={() => { | |
| - setMode((prev) => nextMode(prev)); | |
| - localStorage.setItem('theme', nextMode(mode)); | |
| - }} | |
| - /> | |
| + <Navbar mode={mode} onChangeMode={handleChangeMode} /> | |
| <Suspense fallback={<Loading />}> | |
| <AppRoutes /> | |
| </Suspense> | |
| + <ActionPalette | |
| + open={actionPaletteOpen} | |
| + onClose={() => setActionPaletteOpen(false)} | |
| + mode={mode} | |
| + onChangeMode={handleChangeMode} | |
| + /> | |
| </BrowserRouter> | |
| </UserTypeFilterProvider> | |
| </CustomSnackBarProvider> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment