Enhance CompactChoreCard and MyChores with animated transitions for multi-select and search filter

This commit is contained in:
Mo Tarbin
2025-06-19 02:24:39 -04:00
parent fb8d476c34
commit d8b5e0f5a1
3 changed files with 188 additions and 103 deletions

View File

@@ -399,8 +399,8 @@ const CompactChoreCard = ({
borderBottom: '1px solid',
borderColor: 'divider',
position: 'relative',
pl: isMultiSelectMode ? '48px' : '16px', // Add space for checkbox when in multi-select mode
backgroundColor: 'background.surface',
pl: '16px', // Consistent padding since both elements are in the same position
// backgroundColor: 'background.surface',
transition: 'all 0.2s ease-in-out',
'&:hover': {
bgcolor: 'background.level1',
@@ -428,37 +428,6 @@ const CompactChoreCard = ({
}
}}
>
{/* Multi-select checkbox */}
{isMultiSelectMode && (
<Checkbox
checked={isSelected}
onChange={onSelectionToggle}
sx={{
position: 'absolute',
left: 16,
zIndex: 2,
bgcolor: 'background.surface',
borderRadius: 'md',
boxShadow: 'sm',
border: '2px solid',
borderColor: 'divider',
'&:hover': {
bgcolor: 'background.level1',
borderColor: 'primary.300',
},
'&.Mui-checked': {
bgcolor: 'primary.500',
borderColor: 'primary.500',
color: 'primary.solidColor',
'&:hover': {
bgcolor: 'primary.600',
borderColor: 'primary.600',
},
},
}}
onClick={e => e.stopPropagation()}
/>
)}
{/* Priority bar clickable area */}
{chore.priority > 0 && (
<Box
@@ -477,48 +446,128 @@ const CompactChoreCard = ({
}}
/>
)}
{/* Complete Button - Left side like typical task apps */}
<IconButton
variant='soft'
color='success'
size='sm'
onClick={e => {
e.stopPropagation()
handleTaskCompletion()
}}
disabled={isPendingCompletion || notInCompletionWindow(chore)}
{/* Animated transition container for Complete Button / Multi-select checkbox */}
<Box
sx={{
width: 32,
height: 32,
borderRadius: '50%',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
mr: 1.5,
flexShrink: 0,
bgcolor: 'success.softBg',
color: 'success.600',
border: '1px solid',
borderColor: 'success.200',
transition: 'all 0.2s ease',
'&:hover': {
bgcolor: 'success.solidBg',
color: 'success.solidColor',
borderColor: 'success.400',
transform: 'scale(1.05)',
},
'&:active': {
transform: 'scale(0.95)',
},
'&:disabled': {
opacity: 0.5,
transform: 'none',
},
}}
>
{isPendingCompletion ? (
<CircularProgress size='sm' />
) : (
<Check sx={{ fontSize: 16 }} />
)}
</IconButton>
{/* Complete Button */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition:
'opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
opacity: isMultiSelectMode ? 0 : 1,
transform: isMultiSelectMode
? 'scale(0.8) rotate(45deg)'
: 'scale(1) rotate(0deg)',
pointerEvents: isMultiSelectMode ? 'none' : 'auto',
}}
>
<IconButton
variant='soft'
color='success'
size='sm'
onClick={e => {
e.stopPropagation()
handleTaskCompletion()
}}
disabled={isPendingCompletion || notInCompletionWindow(chore)}
sx={{
width: 32,
height: 32,
borderRadius: '50%',
bgcolor: 'success.softBg',
color: 'success.600',
border: '1px solid',
borderColor: 'success.200',
transition: 'all 0.2s ease',
'&:hover': {
bgcolor: 'success.solidBg',
color: 'success.solidColor',
borderColor: 'success.400',
transform: 'scale(1.05)',
},
'&:active': {
transform: 'scale(0.95)',
},
'&:disabled': {
opacity: 0.5,
transform: 'none',
},
}}
>
{isPendingCompletion ? (
<CircularProgress size='sm' />
) : (
<Check sx={{ fontSize: 16 }} />
)}
</IconButton>
</Box>
{/* Multi-select Checkbox */}
<Box
sx={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition:
'opacity 0.3s ease-in-out, transform 0.3s ease-in-out',
opacity: isMultiSelectMode ? 1 : 0,
transform: isMultiSelectMode
? 'scale(1) rotate(0deg)'
: 'scale(0.8) rotate(-45deg)',
pointerEvents: isMultiSelectMode ? 'auto' : 'none',
}}
>
<Checkbox
checked={isSelected}
onChange={onSelectionToggle}
sx={{
bgcolor: 'background.surface',
borderRadius: 'md',
boxShadow: 'sm',
border: '2px solid',
borderColor: 'divider',
'&:hover': {
bgcolor: 'background.level1',
borderColor: 'primary.300',
},
'&.Mui-checked': {
bgcolor: 'primary.500',
borderColor: 'primary.500',
color: 'primary.solidColor',
'&:hover': {
bgcolor: 'primary.600',
borderColor: 'primary.600',
},
},
}}
onClick={e => e.stopPropagation()}
/>
</Box>
</Box>
{/* Content - Center */}
<Box
@@ -633,31 +682,46 @@ const CompactChoreCard = ({
</Box>
</Box>
{/* Right side - Action Menu only */}
<ChoreActionMenu
variant='plain'
chore={chore}
onChoreUpdate={onChoreUpdate}
onChoreRemove={onChoreRemove}
onCompleteWithNote={() => setIsCompleteWithNoteModalOpen(true)}
onCompleteWithPastDate={() =>
setIsCompleteWithPastDateModalOpen(true)
}
onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)}
onChangeDueDate={() => setIsChangeDueDateModalOpen(true)}
onWriteNFC={() => setIsNFCModalOpen(true)}
onDelete={handleDelete}
{/* Right side - Action Menu with animation */}
<Box
sx={{
width: 32,
height: 32,
color: 'text.tertiary',
flexShrink: 0,
'&:hover': {
color: 'text.secondary',
bgcolor: 'background.level1',
},
transition:
'opacity 0.3s ease-in-out, transform 0.3s ease-in-out, width 0.3s ease-in-out, margin 0.3s ease-in-out',
opacity: isMultiSelectMode ? 0 : 1,
transform: isMultiSelectMode
? 'translateX(20px) scale(0.8)'
: 'translateX(0) scale(1)',
width: isMultiSelectMode ? 0 : 32,
marginRight: isMultiSelectMode ? 0 : undefined,
overflow: 'hidden',
pointerEvents: isMultiSelectMode ? 'none' : 'auto',
}}
/>
>
<ChoreActionMenu
variant='plain'
chore={chore}
onChoreUpdate={onChoreUpdate}
onChoreRemove={onChoreRemove}
onCompleteWithNote={() => setIsCompleteWithNoteModalOpen(true)}
onCompleteWithPastDate={() =>
setIsCompleteWithPastDateModalOpen(true)
}
onChangeAssignee={() => setIsChangeAssigneeModalOpen(true)}
onChangeDueDate={() => setIsChangeDueDateModalOpen(true)}
onWriteNFC={() => setIsNFCModalOpen(true)}
onDelete={handleDelete}
sx={{
width: 32,
height: 32,
color: 'text.tertiary',
flexShrink: 0,
'&:hover': {
color: 'text.secondary',
bgcolor: 'background.level1',
},
}}
/>
</Box>
</Box>
{/* All modals (same as original) */}

View File

@@ -982,7 +982,18 @@ const MyChores = () => {
{isMultiSelectMode ? <CheckBox /> : <CheckBoxOutlineBlank />}
</IconButton>
</Box>
{showSearchFilter && (
{/* Search Filter with animation */}
<Box
sx={{
overflow: 'hidden',
transition: 'all 0.3s ease-in-out',
maxHeight: showSearchFilter ? '150px' : '0',
opacity: showSearchFilter ? 1 : 0,
transform: showSearchFilter ? 'translateY(0)' : 'translateY(-10px)',
marginBottom: showSearchFilter ? 1 : 0,
}}
>
<div className='flex gap-4'>
<div className='grid flex-1 grid-cols-3 gap-4'>
<IconButtonWithMenu
@@ -1101,20 +1112,30 @@ const MyChores = () => {
<CancelRounded />
</IconButton>
</div>
)}
</Box>
{/* Multi-select Toolbar */}
{isMultiSelectMode && (
{/* Multi-select Toolbar with animation */}
<Box
sx={{
position: 'sticky',
top: 0,
zIndex: 1000,
overflow: 'hidden',
transition: 'all 0.3s ease-in-out',
maxHeight: isMultiSelectMode ? '200px' : '0',
opacity: isMultiSelectMode ? 1 : 0,
transform: isMultiSelectMode
? 'translateY(0)'
: 'translateY(-20px)',
marginBottom: isMultiSelectMode ? 2 : 0,
}}
>
<Box
sx={{
position: 'sticky',
top: 0,
zIndex: 1000,
backgroundColor: 'background.surface',
backdropFilter: 'blur(8px)',
borderRadius: 'lg',
p: 2,
mb: 2,
border: '1px solid',
borderColor: 'divider',
boxShadow: 'm',
@@ -1300,7 +1321,7 @@ const MyChores = () => {
</IconButton> */}
</Box>
</Box>
)}
</Box>
{searchFilter !== 'All' && (
<Chip

View File

@@ -384,7 +384,7 @@ const TaskInput = ({ autoFocus, onChoreUpdate, isModalOpen, onClose }) => {
return (
<Modal open={isModalOpen} onClose={handleCloseModal}>
<ModalOverflow>
<ModalDialog size='lg' sx={{ minWidth: '80%' }}>
<ModalDialog size='lg' sx={{ minWidth: '100%' }}>
<Typography level='h4'>Create new task</Typography>
<Chip startDecorator='🚧' variant='soft' color='warning' size='sm'>
Experimental Feature