diff --git a/package-lock.json b/package-lock.json index 0034a390..ed4e5204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,8 @@ "dependencies": { "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "re-resizable": "^6.10.3", + "react-draggable": "^4.4.6", "react-share": "^5.1.0" }, "devDependencies": { @@ -4530,7 +4532,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "dev": true, "engines": { "node": ">=6" } @@ -11503,6 +11504,16 @@ "performance-now": "^2.1.0" } }, + "node_modules/re-resizable": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.10.3.tgz", + "integrity": "sha512-zvWb7X3RJMA4cuSrqoxgs3KR+D+pEXnGrD2FAD6BMYAULnZsSF4b7AOVyG6pC3VVNVOtlagGDCDmZSwWLjjBBw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -11559,6 +11570,20 @@ "react": "^18.2.0" } }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "license": "MIT", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/react-error-boundary": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.12.tgz", @@ -17372,8 +17397,7 @@ "clsx": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "dev": true + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" }, "co": { "version": "4.6.0", @@ -22160,6 +22184,12 @@ "performance-now": "^2.1.0" } }, + "re-resizable": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.10.3.tgz", + "integrity": "sha512-zvWb7X3RJMA4cuSrqoxgs3KR+D+pEXnGrD2FAD6BMYAULnZsSF4b7AOVyG6pC3VVNVOtlagGDCDmZSwWLjjBBw==", + "requires": {} + }, "react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -22206,6 +22236,15 @@ "scheduler": "^0.23.0" } }, + "react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "requires": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + } + }, "react-error-boundary": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.12.tgz", diff --git a/package.json b/package.json index 04cee394..8436340c 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,8 @@ "dependencies": { "js-yaml": "^4.1.0", "lodash": "^4.17.21", + "re-resizable": "^6.10.3", + "react-draggable": "^4.4.6", "react-share": "^5.1.0" } } diff --git a/src/custom/Panel/Panel.tsx b/src/custom/Panel/Panel.tsx new file mode 100644 index 00000000..cc0409f1 --- /dev/null +++ b/src/custom/Panel/Panel.tsx @@ -0,0 +1,103 @@ +import { Resizable } from 're-resizable'; +import React from 'react'; +import Draggable from 'react-draggable'; +import { Box, BoxProps, IconButton, Tooltip } from '../../base'; +import { CloseIcon, CollapseAllIcon, ExpandAllIcon } from '../../icons'; +import { PanelDragHandleIcon } from '../../icons/PanelDragHandle'; +import { useTheme } from '../../theme'; +import { ErrorBoundary } from '../ErrorBoundary'; +import { + DragHandle, + DrawerHeader, + HeaderActionsContainer, + HeaderContainer, + PanelBody, + PanelContainer, + ResizableContent +} from './style'; + +export type PanelProps = { + isOpen: boolean; + children: React.ReactNode; + areAllExpanded?: boolean; + toggleExpandAll?: () => void; + handleClose: () => void; + sx?: BoxProps['sx']; + id?: string; + intitialPosition?: { + left?: string | number; + right?: string | number; + top?: string | number; + bottom?: string | number; + }; +}; + +const Panel_: React.FC = ({ + isOpen, + id = 'panel', + children, + areAllExpanded, + toggleExpandAll, + handleClose, + intitialPosition, + sx +}) => { + const theme = useTheme(); + if (!isOpen) return null; + return ( + + + { + window.dispatchEvent(new Event('panel-resize')); + }} + enable={{ + top: true, + right: true, + bottom: true, + left: true, + topRight: true, + bottomRight: true, + bottomLeft: true, + topLeft: true + }} + > + + +
+ + + {toggleExpandAll && ( + + + {areAllExpanded ? : } + + + )} + + + + + + + + + + + +
+ {children} +
+
+
+
+
+ ); +}; + +export const Panel: React.FC = ({ ...props }) => { + return ; +}; diff --git a/src/custom/Panel/index.tsx b/src/custom/Panel/index.tsx new file mode 100644 index 00000000..0abd9a3c --- /dev/null +++ b/src/custom/Panel/index.tsx @@ -0,0 +1,3 @@ +import { Panel } from './Panel'; + +export { Panel }; diff --git a/src/custom/Panel/style.tsx b/src/custom/Panel/style.tsx new file mode 100644 index 00000000..8f9daf8a --- /dev/null +++ b/src/custom/Panel/style.tsx @@ -0,0 +1,117 @@ +import { ListItemProps } from '@mui/material'; +import { Box, ListItem } from '../../base'; +import { styled } from '../../theme'; +import { PanelProps } from './Panel'; + +export const ListHeader = styled(ListItem)(({ theme }) => ({ + padding: theme.spacing(0.5, 0.5), + marginBlock: theme.spacing(1), + '& .MuiListItemText-primary': { + fontSize: '1rem', + textTransform: 'capitalize', + fontWeight: 700 + }, + cursor: 'pointer', + '&:hover': { + backgroundColor: theme.palette.action.hover + }, + '& .MuiSvgIcon-root': { + opacity: 0, + transition: 'opacity 0.2s' + }, + '&:hover .MuiSvgIcon-root': { + opacity: 1 + } +})); + +interface CustomListItemProps extends ListItemProps { + isVisible: boolean; +} + +export const StyledListItem = styled(ListItem, { + shouldForwardProp: (props) => props !== 'isVisible' +})(({ theme, isVisible }) => ({ + padding: theme.spacing(0.05, 0.5), + fontStyle: isVisible ? 'normal' : 'italic', + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + '& .MuiSvgIcon-root': { + height: 20, + width: 20 + }, + '& .MuiListItemIcon-root': { + minWidth: 0, + opacity: isVisible ? 0.8 : 0.3 + }, + '& .MuiTypography-root': { + fontSize: '0.9rem', + opacity: isVisible ? 1 : 0.5 + } +})); + +export const DrawerHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(4, 2), + alignContent: 'stretch', + justifyContent: 'space-between', + cursor: 'move', + background: + theme.palette.mode === 'light' + ? 'linear-gradient(90deg, #3B687B 0%, #507D90 100%)' + : 'linear-gradient(90deg, #28353A 0%, #3D4F57 100%)', + height: '3rem', + flexShrink: 0 +})); + +export const PanelBody = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), + backgroundColor: theme.palette.background.surfaces, + overflow: 'auto', + flex: 1, + minHeight: 0 +})); + +export const ResizableContent = styled('div')({ + height: '100%', + display: 'flex', + flexDirection: 'column', + minHeight: '3rem' +}); + +export const PanelContainer = styled(Box)<{ intitialPosition: PanelProps['intitialPosition'] }>( + ({ theme, intitialPosition }) => ({ + borderRadius: '8px', + overflow: 'hidden', + flexShrink: 0, + zIndex: 99999, + position: 'absolute', + backgroundColor: theme.palette.background.blur?.light, + boxShadow: '0 4px 16px #05003812', + maxHeight: '80%', + display: 'flex', + boxSizing: 'border-box', + ...intitialPosition + }) +); + +export const DragHandle = styled('div')({ + position: 'absolute', + top: '-3rem', + left: '50%' +}); + +export const HeaderActionsContainer = styled('div')({ + display: 'flex', + gap: '1rem', + justifyContent: 'flex-end', + alignItems: 'center' +}); + +export const HeaderContainer = styled('div')({ + display: 'flex', + justifyContent: 'end', + alignItems: 'center', + flex: '1' +}); diff --git a/src/custom/index.tsx b/src/custom/index.tsx index ac4022ff..0da7c198 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -56,6 +56,7 @@ export { InputSearchField } from './InputSearchField'; export { LearningContent } from './LearningContent'; export { NavigationNavbar } from './NavigationNavbar'; export { Note } from './Note'; +export { Panel } from './Panel'; export { PerformersSection, PerformersSectionButton } from './PerformersSection'; export { SetupPreReq } from './SetupPrerequisite'; export { StyledChapter } from './StyledChapter'; diff --git a/src/icons/PanelDragHandle/PanelDragHandleIcon.tsx b/src/icons/PanelDragHandle/PanelDragHandleIcon.tsx new file mode 100644 index 00000000..31f4a9e2 --- /dev/null +++ b/src/icons/PanelDragHandle/PanelDragHandleIcon.tsx @@ -0,0 +1,34 @@ +import { FC } from 'react'; +import { IconProps } from '../types'; + +export const PanelDragHandleIcon: FC = ({ + height = 24, + width = 24, + fill = '#E8EFF3', + ...props +}) => { + return ( + + + + + + + + + + + ); +}; + +export default PanelDragHandleIcon; diff --git a/src/icons/PanelDragHandle/index.tsx b/src/icons/PanelDragHandle/index.tsx new file mode 100644 index 00000000..3abe60e3 --- /dev/null +++ b/src/icons/PanelDragHandle/index.tsx @@ -0,0 +1 @@ +export { default as PanelDragHandleIcon } from './PanelDragHandleIcon';