refactor(theme): hoist ThemeProvider to App.tsx root
This commit is contained in:
parent
fedbd903ff
commit
07ceef1294
44
src/App.tsx
44
src/App.tsx
@ -1,5 +1,7 @@
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { ThemeProvider } from '@mui/material/styles';
|
import { useMemo } from 'react';
|
||||||
|
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
|
import { useTLDraw } from './contexts/TLDrawContext';
|
||||||
import { theme } from './services/themeService';
|
import { theme } from './services/themeService';
|
||||||
import { AuthProvider } from './contexts/AuthContext';
|
import { AuthProvider } from './contexts/AuthContext';
|
||||||
import { TLDrawProvider } from './contexts/TLDrawContext';
|
import { TLDrawProvider } from './contexts/TLDrawContext';
|
||||||
@ -8,21 +10,43 @@ import AppRoutes from './AppRoutes';
|
|||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
const App = React.memo(() => (
|
const App = React.memo(() => {
|
||||||
<ErrorBoundary>
|
const { tldrawPreferences } = useTLDraw();
|
||||||
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
|
||||||
<ThemeProvider theme={theme}>
|
const prefersDarkMode =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
const appTheme = useMemo(() => {
|
||||||
|
let mode: 'light' | 'dark';
|
||||||
|
|
||||||
|
if (tldrawPreferences?.colorScheme === 'system') {
|
||||||
|
mode = prefersDarkMode ? 'dark' : 'light';
|
||||||
|
} else {
|
||||||
|
mode = tldrawPreferences?.colorScheme === 'dark' ? 'dark' : 'light';
|
||||||
|
}
|
||||||
|
|
||||||
|
return createTheme({
|
||||||
|
palette: { mode },
|
||||||
|
});
|
||||||
|
}, [tldrawPreferences?.colorScheme, prefersDarkMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<TLDrawProvider>
|
<TLDrawProvider>
|
||||||
<AppRoutes />
|
<ThemeProvider theme={appTheme}>
|
||||||
|
<AppRoutes />
|
||||||
|
</ThemeProvider>
|
||||||
</TLDrawProvider>
|
</TLDrawProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</ThemeProvider>
|
</BrowserRouter>
|
||||||
</BrowserRouter>
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
);
|
||||||
));
|
});
|
||||||
|
|
||||||
App.displayName = import.meta.env.VITE_APP_NAME;
|
App.displayName = import.meta.env.VITE_APP_NAME;
|
||||||
|
|
||||||
|
|||||||
@ -169,8 +169,8 @@ const AppRoutes: React.FC = () => {
|
|||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Fallback route - use different NotFound pages based on auth state */}
|
{/* Fallback route - authenticated users go to dashboard, unauthenticated see public 404 */}
|
||||||
<Route path="*" element={user ? <NotFound /> : <NotFoundPublic />} />
|
<Route path="*" element={user ? <Navigate to="/dashboard" replace /> : <NotFoundPublic />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
IconButton,
|
IconButton,
|
||||||
Box,
|
Box,
|
||||||
|
Chip,
|
||||||
useTheme,
|
useTheme,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@ -126,6 +127,15 @@ const Header: React.FC = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||||
|
{isAuthenticated && bootstrapData?.onboarding?.next_step && bootstrapData.onboarding.next_step !== 'ready' && (
|
||||||
|
<Chip
|
||||||
|
label={bootstrapData.onboarding.next_step.replace(/_/g, ' ')}
|
||||||
|
color="warning"
|
||||||
|
size="small"
|
||||||
|
onClick={() => navigate('/dashboard')}
|
||||||
|
sx={{ cursor: 'pointer', display: { xs: 'none', sm: 'flex' } }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useRef, useMemo } from 'react';
|
import React, { useEffect, useRef, useMemo } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
import { TldrawUiButton } from '@tldraw/tldraw';
|
import { TldrawUiButton } from '@tldraw/tldraw';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -8,9 +9,6 @@ import {
|
|||||||
ListItemIcon,
|
ListItemIcon,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
styled,
|
styled,
|
||||||
ThemeProvider,
|
|
||||||
createTheme,
|
|
||||||
useMediaQuery
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {
|
import {
|
||||||
PushPin as PushPinIcon,
|
PushPin as PushPinIcon,
|
||||||
@ -24,7 +22,7 @@ import {
|
|||||||
Navigation as NavigationIcon,
|
Navigation as NavigationIcon,
|
||||||
Save as NodeIcon,
|
Save as NodeIcon,
|
||||||
Assignment as ExamIcon,
|
Assignment as ExamIcon,
|
||||||
Mic as MicIcon
|
Mic as MicIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
import { CCShapesPanel } from './CCShapesPanel';
|
import { CCShapesPanel } from './CCShapesPanel';
|
||||||
import { CCSlidesPanel } from './CCSlidesPanel';
|
import { CCSlidesPanel } from './CCSlidesPanel';
|
||||||
@ -95,7 +93,7 @@ const PanelTypeButton = styled(Button)(() => ({
|
|||||||
'& .MuiSvgIcon-root': {
|
'& .MuiSvgIcon-root': {
|
||||||
fontSize: '1.25rem',
|
fontSize: '1.25rem',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledMenuItem = styled(MenuItem)(() => ({
|
const StyledMenuItem = styled(MenuItem)(() => ({
|
||||||
@ -106,7 +104,7 @@ const StyledMenuItem = styled(MenuItem)(() => ({
|
|||||||
backgroundColor: 'var(--color-hover)',
|
backgroundColor: 'var(--color-hover)',
|
||||||
'& .MuiListItemIcon-root': {
|
'& .MuiListItemIcon-root': {
|
||||||
color: 'var(--color-selected)',
|
color: 'var(--color-selected)',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
'& .MuiListItemIcon-root': {
|
'& .MuiListItemIcon-root': {
|
||||||
color: 'var(--color-text)',
|
color: 'var(--color-text)',
|
||||||
@ -114,8 +112,8 @@ const StyledMenuItem = styled(MenuItem)(() => ({
|
|||||||
transition: 'color 200ms ease',
|
transition: 'color 200ms ease',
|
||||||
'& .MuiSvgIcon-root': {
|
'& .MuiSvgIcon-root': {
|
||||||
fontSize: '1.25rem',
|
fontSize: '1.25rem',
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const BasePanel: React.FC<BasePanelProps> = ({
|
export const BasePanel: React.FC<BasePanelProps> = ({
|
||||||
@ -130,34 +128,24 @@ export const BasePanel: React.FC<BasePanelProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { tldrawPreferences } = useTLDraw();
|
const { tldrawPreferences } = useTLDraw();
|
||||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
const [menuAnchorEl, setMenuAnchorEl] = React.useState<null | HTMLElement>(null);
|
const [menuAnchorEl, setMenuAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
// Create a dynamic theme based on TLDraw preferences
|
// Mui theme object from root provider
|
||||||
const theme = useMemo(() => {
|
const computedTheme = useTheme();
|
||||||
let mode: 'light' | 'dark';
|
const accentColor = useMemo(() => {
|
||||||
|
if (tldrawPreferences?.colorScheme === 'dark') return '#e2e8f0';
|
||||||
if (tldrawPreferences?.colorScheme === 'system') {
|
if (tldrawPreferences?.colorScheme === 'system') return prefersDarkMode ? '#e2e8f0' : '#1e293b';
|
||||||
mode = prefersDarkMode ? 'dark' : 'light';
|
return '#1e293b';
|
||||||
} else {
|
|
||||||
mode = tldrawPreferences?.colorScheme === 'dark' ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
return createTheme({
|
|
||||||
palette: {
|
|
||||||
mode,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [tldrawPreferences?.colorScheme, prefersDarkMode]);
|
}, [tldrawPreferences?.colorScheme, prefersDarkMode]);
|
||||||
|
|
||||||
const isExamMarkerRoute = location.pathname === '/exam-marker';
|
const isExamMarkerRoute = location.pathname === '/exam-marker';
|
||||||
const availablePanels = isExamMarkerRoute ? PANEL_TYPES.examMarker : PANEL_TYPES.default;
|
const availablePanels = isExamMarkerRoute ? PANEL_TYPES.examMarker : PANEL_TYPES.default;
|
||||||
|
|
||||||
const [currentPanelType, setCurrentPanelType] = React.useState<PanelType>(
|
const [currentPanelType, setCurrentPanelType] = React.useState<PanelType>(
|
||||||
isExamMarkerRoute ? 'exam-marker' : initialPanelType
|
isExamMarkerRoute ? 'exam-marker' : initialPanelType,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use controlled state if provided, otherwise use internal state
|
|
||||||
const [internalIsExpanded, setInternalIsExpanded] = React.useState(true);
|
const [internalIsExpanded, setInternalIsExpanded] = React.useState(true);
|
||||||
const [internalIsPinned, setInternalIsPinned] = React.useState(true);
|
const [internalIsPinned, setInternalIsPinned] = React.useState(true);
|
||||||
|
|
||||||
@ -178,16 +166,11 @@ export const BasePanel: React.FC<BasePanelProps> = ({
|
|||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const dimensions = PANEL_DIMENSIONS[currentPanelType as keyof typeof PANEL_DIMENSIONS];
|
const dimensions = PANEL_DIMENSIONS[currentPanelType as keyof typeof PANEL_DIMENSIONS];
|
||||||
|
|
||||||
// Handle click outside
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
// Don't close if pinned
|
|
||||||
if (isPinned) return;
|
if (isPinned) return;
|
||||||
|
|
||||||
// Check if click is outside panel
|
|
||||||
const isClickOutside = panelRef.current && !panelRef.current.contains(event.target as Node);
|
const isClickOutside = panelRef.current && !panelRef.current.contains(event.target as Node);
|
||||||
|
|
||||||
// Check if click is not on a panel-related element
|
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
const isPanelElement = target.closest('.panel-root, .panel-handle, .tlui-button');
|
const isPanelElement = target.closest('.panel-root, .panel-handle, .tlui-button');
|
||||||
|
|
||||||
@ -288,13 +271,11 @@ export const BasePanel: React.FC<BasePanelProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle menu button click
|
|
||||||
const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => {
|
const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||||
setMenuAnchorEl(event.currentTarget);
|
setMenuAnchorEl(event.currentTarget);
|
||||||
onMenuOpenChange(true);
|
onMenuOpenChange(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle menu close
|
|
||||||
const handleMenuClose = () => {
|
const handleMenuClose = () => {
|
||||||
setMenuAnchorEl(null);
|
setMenuAnchorEl(null);
|
||||||
onMenuOpenChange(false);
|
onMenuOpenChange(false);
|
||||||
@ -327,30 +308,29 @@ export const BasePanel: React.FC<BasePanelProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<ThemeProvider theme={theme}>
|
<PanelTypeButton
|
||||||
<PanelTypeButton
|
onClick={handleMenuClick}
|
||||||
onClick={handleMenuClick}
|
endIcon={<ExpandMoreIcon />}
|
||||||
endIcon={<ExpandMoreIcon />}
|
startIcon={getIconForPanel(currentPanelType)}
|
||||||
startIcon={getIconForPanel(currentPanelType)}
|
>
|
||||||
>
|
{availablePanels.find((p) => p.id === currentPanelType)?.label}
|
||||||
{availablePanels.find(p => p.id === currentPanelType)?.label}
|
</PanelTypeButton>
|
||||||
</PanelTypeButton>
|
|
||||||
|
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={menuAnchorEl}
|
anchorEl={menuAnchorEl}
|
||||||
open={isMenuOpen}
|
open={isMenuOpen}
|
||||||
onClose={handleMenuClose}
|
onClose={handleMenuClose}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
sx: {
|
sx: {
|
||||||
border: '1px solid var(--color-divider)',
|
border: '1px solid var(--color-divider)',
|
||||||
boxShadow: 'var(--shadow-popup)',
|
boxShadow: 'var(--shadow-popup)',
|
||||||
}
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[...availablePanels]
|
{[...availablePanels]
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order)
|
||||||
.map(type => (
|
.map((type) => (
|
||||||
<StyledMenuItem
|
<StyledMenuItem
|
||||||
key={type.id}
|
key={type.id}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -359,40 +339,29 @@ export const BasePanel: React.FC<BasePanelProps> = ({
|
|||||||
}}
|
}}
|
||||||
selected={currentPanelType === type.id}
|
selected={currentPanelType === type.id}
|
||||||
>
|
>
|
||||||
<ListItemIcon>
|
<ListItemIcon>{getIconForPanel(type.id as PanelType)}</ListItemIcon>
|
||||||
{getIconForPanel(type.id as PanelType)}
|
|
||||||
</ListItemIcon>
|
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary={type.label}
|
primary={type.label}
|
||||||
secondary={getDescriptionForPanel(type.id as PanelType)}
|
secondary={getDescriptionForPanel(type.id as PanelType)}
|
||||||
primaryTypographyProps={{
|
primaryTypographyProps={{
|
||||||
sx: { color: 'var(--color-text)' }
|
sx: { color: 'var(--color-text)' },
|
||||||
}}
|
}}
|
||||||
secondaryTypographyProps={{
|
secondaryTypographyProps={{
|
||||||
sx: { color: 'var(--color-text-secondary)' }
|
sx: { color: 'var(--color-text-secondary)' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StyledMenuItem>
|
</StyledMenuItem>
|
||||||
))}
|
))}
|
||||||
</Menu>
|
</Menu>
|
||||||
</ThemeProvider>
|
|
||||||
|
|
||||||
<div className="panel-header-actions">
|
<div className="panel-header-actions">
|
||||||
<TldrawUiButton
|
<TldrawUiButton type="icon" onClick={handlePinToggle} className="pin-button">
|
||||||
type="icon"
|
|
||||||
onClick={handlePinToggle}
|
|
||||||
className="pin-button"
|
|
||||||
>
|
|
||||||
{isPinned ? <PushPinIcon /> : <PushPinOutlinedIcon />}
|
{isPinned ? <PushPinIcon /> : <PushPinOutlinedIcon />}
|
||||||
</TldrawUiButton>
|
</TldrawUiButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeProvider theme={theme}>
|
<div className="panel-content">{renderCurrentPanel()}</div>
|
||||||
<div className="panel-content">
|
|
||||||
{renderCurrentPanel()}
|
|
||||||
</div>
|
|
||||||
</ThemeProvider>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user