merge: remove MUI/styled-components from BasePanel — cc- CSS classes (P2a)

This commit is contained in:
CC Worker 2026-05-31 22:22:31 +00:00
commit f1c1a72f44

View File

@ -1,31 +1,6 @@
import React, { useEffect, useRef, useMemo } from 'react'; import React, { useEffect, useRef, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { TldrawUiButton } from '@tldraw/tldraw'; import { TldrawUiButton } from '@tldraw/tldraw';
import {
Button,
Menu,
MenuItem,
ListItemIcon,
ListItemText,
styled,
ThemeProvider,
createTheme,
useMediaQuery
} from '@mui/material';
import {
PushPin as PushPinIcon,
PushPinOutlined as PushPinOutlinedIcon,
ExpandMore as ExpandMoreIcon,
Category as ShapesIcon,
Slideshow as SlidesIcon,
YouTube as YouTubeIcon,
AccountTree as GraphIcon,
Search as SearchIcon,
Navigation as NavigationIcon,
Save as NodeIcon,
Assignment as ExamIcon,
Mic as MicIcon
} from '@mui/icons-material';
import { CCShapesPanel } from './CCShapesPanel'; import { CCShapesPanel } from './CCShapesPanel';
import { CCSlidesPanel } from './CCSlidesPanel'; import { CCSlidesPanel } from './CCSlidesPanel';
import { CCFilesPanel } from './CCFilesPanel'; import { CCFilesPanel } from './CCFilesPanel';
@ -33,9 +8,9 @@ import { CCCabinetsPanel } from './CCCabinetsPanel';
import { CCYoutubePanel } from './CCYoutubePanel'; import { CCYoutubePanel } from './CCYoutubePanel';
import { CCGraphPanel } from './CCGraphPanel'; import { CCGraphPanel } from './CCGraphPanel';
import { CCExamMarkerPanel } from './CCExamMarkerPanel'; import { CCExamMarkerPanel } from './CCExamMarkerPanel';
import { CCSearchPanel } from './CCSearchPanel' import { CCSearchPanel } from './CCSearchPanel';
import { CCGraphNavPanel } from './navigation/CCGraphNavPanel' import { CCGraphNavPanel } from './navigation/CCGraphNavPanel';
import { CCTranscriptionPanel } from './CCTranscriptionPanel' import { CCTranscriptionPanel } from './CCTranscriptionPanel';
import { PANEL_DIMENSIONS, Z_INDICES } from './panel-styles'; import { PANEL_DIMENSIONS, Z_INDICES } from './panel-styles';
import './panel.css'; import './panel.css';
// import { CCNavigationPanel } from './navigation/CCNavigationPanel'; // import { CCNavigationPanel } from './navigation/CCNavigationPanel';
@ -78,45 +53,102 @@ interface BasePanelProps {
onMenuOpenChange?: (open: boolean) => void; onMenuOpenChange?: (open: boolean) => void;
} }
const PanelTypeButton = styled(Button)(() => ({ function getIconSvg(panelId: PanelType, size = '1.25rem') {
textTransform: 'none', const common = { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const };
padding: '6px 12px', switch (panelId) {
gap: '8px', case 'cabinets':
backgroundColor: 'var(--color-panel)', return (
color: 'var(--color-text)', <svg {...common}>
border: '1px solid transparent', <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
transition: 'border-color 200ms ease', <polyline points="9 22 9 12 15 12 15 22" />
justifyContent: 'space-between', </svg>
minWidth: '200px', );
'&:hover': { case 'transcription':
backgroundColor: 'var(--color-panel)', return (
borderColor: 'var(--color-text)', <svg {...common}>
}, <path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
'& .MuiSvgIcon-root': { <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
fontSize: '1.25rem', <line x1="12" y1="19" x2="12" y2="23" />
color: 'inherit', <line x1="8" y1="23" x2="6" y2="21" />
<line x1="16" y1="23" x2="14" y2="21" />
</svg>
);
case 'cc-shapes':
return (
<svg {...common}>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
);
case 'slides':
return (
<svg {...common}>
<rect x="2" y="3" width="13" height="18" rx="2" />
<path d="M15 8h4l3 3v7h-7V8z" />
</svg>
);
case 'youtube':
return (
<svg {...common}>
<rect x="2" y="4" width="20" height="16" rx="4" />
<polygon points="10 8 16 12 10 16 10 8" fill="currentColor" stroke="none" />
</svg>
);
case 'graph':
return (
<svg {...common}>
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
</svg>
);
case 'search':
return (
<svg {...common}>
<circle cx="11" cy="11" r="8" />
<line x1="21" y1="21" x2="16.65" y2="16.65" />
</svg>
);
case 'navigation':
return (
<svg {...common}>
<polygon points="3 11 22 2 13 21 11 13 3 11" />
</svg>
);
case 'node-snapshot':
return (
<svg {...common}>
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
</svg>
);
case 'exam-marker':
return (
<svg {...common}>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<polyline points="10 9 9 9 8 9" />
</svg>
);
default:
return (
<svg {...common}>
<rect x="3" y="3" width="18" height="18" rx="2" />
</svg>
);
}
} }
}));
const StyledMenuItem = styled(MenuItem)(() => ({ function isDarkMode() {
gap: '8px', if (typeof window === 'undefined') return false;
padding: '8px 16px', return document.documentElement.dataset.colorMode === 'dark' ||
transition: 'background-color 200ms ease', window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
'&:hover': {
backgroundColor: 'var(--color-hover)',
'& .MuiListItemIcon-root': {
color: 'var(--color-selected)',
} }
},
'& .MuiListItemIcon-root': {
color: 'var(--color-text)',
minWidth: '32px',
transition: 'color 200ms ease',
'& .MuiSvgIcon-root': {
fontSize: '1.25rem',
}
}
}));
export const BasePanel: React.FC<BasePanelProps> = ({ export const BasePanel: React.FC<BasePanelProps> = ({
initialPanelType = 'files', initialPanelType = 'files',
@ -130,36 +162,30 @@ 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, setPrefersDarkMode] = useState(isDarkMode());
const [menuAnchorEl, setMenuAnchorEl] = React.useState<null | HTMLElement>(null); const [menuOpen, setMenuOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
// Create a dynamic theme based on TLDraw preferences useEffect(() => {
const theme = useMemo(() => { const mq = window.matchMedia('(prefers-color-scheme: dark)');
let mode: 'light' | 'dark'; const handler = () => setPrefersDarkMode(isDarkMode());
mq.addEventListener?.('change', handler);
return () => mq.removeEventListener?.('change', handler);
}, []);
if (tldrawPreferences?.colorScheme === 'system') { const colorMode = (tldrawPreferences?.colorScheme === 'system' ? prefersDarkMode : tldrawPreferences?.colorScheme === 'dark')
mode = prefersDarkMode ? 'dark' : 'light'; ? 'dark'
} else { : 'light';
mode = tldrawPreferences?.colorScheme === 'dark' ? 'dark' : 'light';
}
return createTheme({
palette: {
mode,
},
});
}, [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] = useState<PanelType>(
isExamMarkerRoute ? 'exam-marker' : initialPanelType isExamMarkerRoute ? 'exam-marker' : initialPanelType
); );
// Use controlled state if provided, otherwise use internal state const [internalIsExpanded, setInternalIsExpanded] = useState(true);
const [internalIsExpanded, setInternalIsExpanded] = React.useState(true); const [internalIsPinned, setInternalIsPinned] = useState(true);
const [internalIsPinned, setInternalIsPinned] = React.useState(true);
const isExpanded = controlledIsExpanded ?? internalIsExpanded; const isExpanded = controlledIsExpanded ?? internalIsExpanded;
const isPinned = controlledIsPinned ?? internalIsPinned; const isPinned = controlledIsPinned ?? internalIsPinned;
@ -178,87 +204,24 @@ 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');
if (isClickOutside && !isPanelElement) { if (isClickOutside && !isPanelElement) {
handleExpandedChange(false); handleExpandedChange(false);
} }
}; };
if (isExpanded) { if (isExpanded) {
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
} }
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('mousedown', handleClickOutside);
}; };
}, [isExpanded, isPinned]); }, [isExpanded, isPinned]);
const getIconForPanel = (panelId: PanelType) => {
switch (panelId) {
case 'cabinets':
return <NavigationIcon />;
case 'cc-shapes':
return <ShapesIcon />;
case 'transcription':
return <MicIcon />;
case 'slides':
return <SlidesIcon />;
case 'youtube':
return <YouTubeIcon />;
case 'graph':
return <GraphIcon />;
case 'search':
return <SearchIcon />;
case 'navigation':
return <NavigationIcon />;
case 'node-snapshot':
return <NodeIcon />;
case 'exam-marker':
return <ExamIcon />;
default:
return <ShapesIcon />;
}
};
const getDescriptionForPanel = (panelId: PanelType) => {
switch (panelId) {
case 'cabinets':
return 'Manage file cabinets';
case 'transcription':
return 'Record and transcribe lessons';
case 'cc-shapes':
return 'Add shapes and elements to your canvas';
case 'slides':
return 'Manage presentation slides';
case 'youtube':
return 'Embed YouTube videos';
case 'graph':
return 'View and manage graph connections';
case 'search':
return 'Search through your content';
case 'navigation':
return 'Navigate through different contexts';
case 'node-snapshot':
return 'Manage node snapshots';
case 'exam-marker':
return 'Mark and grade exams';
default:
return '';
}
};
const renderCurrentPanel = () => { const renderCurrentPanel = () => {
if (isExamMarkerRoute && currentPanelType === 'exam-marker') { if (isExamMarkerRoute && currentPanelType === 'exam-marker') {
return examMarkerProps ? <CCExamMarkerPanel {...examMarkerProps} /> : null; return examMarkerProps ? <CCExamMarkerPanel {...examMarkerProps} /> : null;
@ -288,16 +251,9 @@ export const BasePanel: React.FC<BasePanelProps> = ({
} }
}; };
// Handle menu button click const toggleMenu = (open: boolean) => {
const handleMenuClick = (event: React.MouseEvent<HTMLElement>) => { setMenuOpen(open);
setMenuAnchorEl(event.currentTarget); onMenuOpenChange(open);
onMenuOpenChange(true);
};
// Handle menu close
const handleMenuClose = () => {
setMenuAnchorEl(null);
onMenuOpenChange(false);
}; };
return ( return (
@ -327,55 +283,61 @@ export const BasePanel: React.FC<BasePanelProps> = ({
}} }}
> >
<div className="panel-header"> <div className="panel-header">
<ThemeProvider theme={theme}> <button
<PanelTypeButton type="button"
onClick={handleMenuClick} className="cc-btn cc-btn--secondary cc-panel-selector"
endIcon={<ExpandMoreIcon />} onClick={() => toggleMenu(!menuOpen)}
startIcon={getIconForPanel(currentPanelType)}
> >
<span className="cc-btn-start-icon">
{getIconSvg(currentPanelType)}
</span>
<span className="cc-btn-label">
{availablePanels.find(p => p.id === currentPanelType)?.label} {availablePanels.find(p => p.id === currentPanelType)?.label}
</PanelTypeButton> </span>
<span className="cc-btn-end-icon">
<svg width="1.25rem" height="1.25rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="6 9 12 15 18 9" />
</svg>
</span>
</button>
<Menu {menuOpen && (
anchorEl={menuAnchorEl} <div className="cc-menu panel-type-menu" role="menu">
open={isMenuOpen}
onClose={handleMenuClose}
PaperProps={{
elevation: 8,
sx: {
border: '1px solid var(--color-divider)',
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 <button
key={type.id} key={type.id}
type="button"
role="menuitem"
className={`cc-menu-item ${type.id === currentPanelType ? 'cc-menu-item--active' : ''}`}
onClick={() => { onClick={() => {
setCurrentPanelType(type.id as PanelType); setCurrentPanelType(type.id as PanelType);
handleMenuClose(); toggleMenu(false);
}} }}
selected={currentPanelType === type.id}
> >
<ListItemIcon> <span className="cc-menu-item-icon">
{getIconForPanel(type.id as PanelType)} {getIconSvg(type.id as PanelType)}
</ListItemIcon> </span>
<ListItemText <span className="cc-menu-item-text">
primary={type.label} <span className="cc-menu-item-primary">{type.label}</span>
secondary={getDescriptionForPanel(type.id as PanelType)} <span className="cc-menu-item-secondary">
primaryTypographyProps={{ {type.id === 'cabinets' && 'Manage file cabinets'}
sx: { color: 'var(--color-text)' } {type.id === 'transcription' && 'Record and transcribe lessons'}
}} {type.id === 'cc-shapes' && 'Add shapes and elements to your canvas'}
secondaryTypographyProps={{ {type.id === 'slides' && 'Manage presentation slides'}
sx: { color: 'var(--color-text-secondary)' } {type.id === 'youtube' && 'Embed YouTube videos'}
}} {type.id === 'graph' && 'View and manage graph connections'}
/> {type.id === 'search' && 'Search through your content'}
</StyledMenuItem> {type.id === 'navigation' && 'Navigate through different contexts'}
{type.id === 'node-snapshot' && 'Manage node snapshots'}
{type.id === 'exam-marker' && 'Mark and grade exams'}
</span>
</span>
</button>
))} ))}
</Menu> </div>
</ThemeProvider> )}
<div className="panel-header-actions"> <div className="panel-header-actions">
<TldrawUiButton <TldrawUiButton
@ -383,16 +345,36 @@ export const BasePanel: React.FC<BasePanelProps> = ({
onClick={handlePinToggle} onClick={handlePinToggle}
className="pin-button" className="pin-button"
> >
{isPinned ? <PushPinIcon /> : <PushPinOutlinedIcon />} <span className="cc-icon-button">
<svg
width="1.25rem"
height="1.25rem"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
{isPinned ? (
<>
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87L18.18 22 12 18.56 5.82 22 7 14.14l-5-4.87 6.91-1.01L12 2z" />
</>
) : (
<>
<circle cx="12" cy="12" r="10" />
<path d="M7.5 7.5l9 9" />
</>
)}
</svg>
</span>
</TldrawUiButton> </TldrawUiButton>
</div> </div>
</div> </div>
<ThemeProvider theme={theme}>
<div className="panel-content"> <div className="panel-content">
{renderCurrentPanel()} {renderCurrentPanel()}
</div> </div>
</ThemeProvider>
</div> </div>
)} )}
</> </>