Header checkbox states

This commit is contained in:
Jackson Harper
2024-02-16 11:37:41 +08:00
parent ded8108a15
commit 72c66c4024
2 changed files with 356 additions and 274 deletions

View File

@ -4,8 +4,28 @@ import { IconProps } from './IconProps'
import { SpanBox } from '../LayoutPrimitives'
import React from 'react'
import { MultiSelectMode } from '../../templates/homeFeed/LibraryHeader'
export class HeaderCheckboxIcon extends React.Component<IconProps> {
type HeaderCheckboxIconProps = {
multiSelectMode: MultiSelectMode
}
export const HeaderCheckboxIcon = (
props: HeaderCheckboxIconProps
): JSX.Element => {
switch (props.multiSelectMode) {
case 'search':
case 'visible':
return <HeaderCheckboxCheckedIcon />
case 'none':
case 'off':
return <HeaderCheckboxUncheckedIcon />
case 'some':
return <HeaderCheckboxHalfCheckedIcon />
}
}
export class HeaderCheckboxUncheckedIcon extends React.Component<IconProps> {
render() {
return (
<SpanBox
@ -54,3 +74,121 @@ export class HeaderCheckboxIcon extends React.Component<IconProps> {
)
}
}
export class HeaderCheckboxCheckedIcon extends React.Component<IconProps> {
render() {
return (
<SpanBox
css={{
display: 'flex',
'--inner-color': 'var(--colors-thHeaderIconInner)',
'--ring-color': 'var(--colors-thHeaderIconRing)',
'&:hover': {
'--ring-fill': '#007AFF10',
},
}}
>
<svg
width="41"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: 'var(--ring-fill)',
stroke: 'var(--ring-color)',
}}
/>
<g>
<path
d="M25.9341 11.6667C27.5674 11.6667 28.9007 12.9476 28.9857 14.5601L28.9899 14.7226V25.2776C28.9899 26.9109 27.7091 28.2442 26.0966 28.3292L25.9341 28.3334H15.3791C14.5967 28.3335 13.8442 28.0334 13.2764 27.4951C12.7087 26.9569 12.369 26.2213 12.3274 25.4401L12.3232 25.2776V14.7226C12.3232 13.0892 13.6041 11.7559 15.2166 11.6709L15.3791 11.6667H25.9341ZM23.7457 17.7442C23.5895 17.588 23.3775 17.5003 23.1566 17.5003C22.9356 17.5003 22.7237 17.588 22.5674 17.7442L19.8232 20.4876L18.7457 19.4109L18.6674 19.3417C18.4999 19.2122 18.2894 19.1513 18.0786 19.1714C17.8679 19.1915 17.6726 19.291 17.5326 19.4498C17.3926 19.6087 17.3183 19.8148 17.3247 20.0264C17.3312 20.2381 17.418 20.4393 17.5674 20.5892L19.2341 22.2559L19.3124 22.3251C19.4727 22.4495 19.673 22.5111 19.8755 22.4983C20.078 22.4856 20.2689 22.3994 20.4124 22.2559L23.7457 18.9226L23.8149 18.8442C23.9393 18.6839 24.0009 18.4837 23.9881 18.2812C23.9754 18.0787 23.8892 17.8877 23.7457 17.7442Z"
fill="#007AFF"
/>
</g>
</svg>
</SpanBox>
)
}
}
export class HeaderCheckboxHalfCheckedIcon extends React.Component<IconProps> {
render() {
return (
<SpanBox
css={{
display: 'flex',
'--ring-fill': '#007AFF',
'--inner-color': '#007AFF',
'--ring-color': 'var(--colors-thHeaderIconRing)',
// '&:hover': {
// '--inner-color': 'white',
// '--ring-color': '#007AFF',
// },
}}
>
<svg
width="41"
height="40"
viewBox="0 0 41 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="1.15625"
y="0.5"
width="39"
height="39"
rx="19.5"
stroke="#3D3D3D"
/>
<g>
<path
d="M25.9341 11.6667C27.5674 11.6667 28.9007 12.9476 28.9857 14.5601L28.9899 14.7226V25.2776C28.9899 26.9109 27.7091 28.2442 26.0966 28.3292L25.9341 28.3334H15.3791C14.5967 28.3335 13.8442 28.0334 13.2764 27.4951C12.7087 26.9569 12.369 26.2213 12.3274 25.4401L12.3232 25.2776V14.7226C12.3232 13.0892 13.6041 11.7559 15.2166 11.6709L15.3791 11.6667H25.9341ZM23.7457 17.7442C23.5895 17.588 23.3775 17.5003 23.1566 17.5003C22.9356 17.5003 22.7237 17.588 22.5674 17.7442L19.8232 20.4876L18.7457 19.4109L18.6674 19.3417C18.4999 19.2122 18.2894 19.1513 18.0786 19.1714C17.8679 19.1915 17.6726 19.291 17.5326 19.4498C17.3926 19.6087 17.3183 19.8148 17.3247 20.0264C17.3312 20.2381 17.418 20.4393 17.5674 20.5892L19.2341 22.2559L19.3124 22.3251C19.4727 22.4495 19.673 22.5111 19.8755 22.4983C20.078 22.4856 20.2689 22.3994 20.4124 22.2559L23.7457 18.9226L23.8149 18.8442C23.9393 18.6839 24.0009 18.4837 23.9881 18.2812C23.9754 18.0787 23.8892 17.8877 23.7457 17.7442Z"
fill="#007AFF"
/>
</g>
</svg>
{/* <svg
width="41"
height="40"
viewBox="0 0 40 40"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="0.5"
width="39"
height="39"
rx="19.5"
style={{
fill: '#007AFF',
stroke: 'var(--ring-color)',
}}
/>
<g>
<path
d="M25.9341 11.6667C27.5674 11.6667 28.9007 12.9476 28.9857 14.5601L28.9899 14.7226V25.2776C28.9899 26.9109 27.7091 28.2442 26.0966 28.3292L25.9341 28.3334H15.3791C14.5967 28.3335 13.8442 28.0334 13.2764 27.4951C12.7087 26.9569 12.369 26.2213 12.3274 25.4401L12.3232 25.2776V14.7226C12.3232 13.0892 13.6041 11.7559 15.2166 11.6709L15.3791 11.6667H25.9341ZM23.7457 17.7442C23.5895 17.588 23.3775 17.5003 23.1566 17.5003C22.9356 17.5003 22.7237 17.588 22.5674 17.7442L19.8232 20.4876L18.7457 19.4109L18.6674 19.3417C18.4999 19.2122 18.2894 19.1513 18.0786 19.1714C17.8679 19.1915 17.6726 19.291 17.5326 19.4498C17.3926 19.6087 17.3183 19.8148 17.3247 20.0264C17.3312 20.2381 17.418 20.4393 17.5674 20.5892L19.2341 22.2559L19.3124 22.3251C19.4727 22.4495 19.673 22.5111 19.8755 22.4983C20.078 22.4856 20.2689 22.3994 20.4124 22.2559L23.7457 18.9226L23.8149 18.8442C23.9393 18.6839 24.0009 18.4837 23.9881 18.2812C23.9754 18.0787 23.8892 17.8877 23.7457 17.7442Z"
style={{
fill: '#007AFF',
stroke: 'var(--inner-color)',
}}
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg> */}
</SpanBox>
)
}
}

View File

@ -1,24 +1,15 @@
import { useEffect, useMemo, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { theme } from '../../tokens/stitches.config'
import { FormInput } from '../../elements/FormElements'
import { searchBarCommands } from '../../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { locale, timeZone } from '../../../lib/dateFormatting'
import { Button, IconButton } from '../../elements/Button'
import {
FunnelSimple,
MagnifyingGlass,
Prohibit,
Plus,
X,
} from 'phosphor-react'
import { FunnelSimple, MagnifyingGlass, Plus, X } from 'phosphor-react'
import { LayoutType } from './HomeFeedContainer'
import { OmnivoreSmallLogo } from '../../elements/images/OmnivoreNameLogo'
import { DEFAULT_HEADER_HEIGHT, HeaderSpacer } from './HeaderSpacer'
import { LIBRARY_LEFT_MENU_WIDTH } from '../navMenu/LibraryMenu'
import { CardCheckbox } from '../../patterns/LibraryCards/LibraryCardStyles'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
import { ConfirmationModal } from '../../patterns/ConfirmationModal'
import { AddBulkLabelsModal } from '../article/AddBulkLabelsModal'
@ -26,13 +17,10 @@ import { Label } from '../../../lib/networking/fragments/labelFragment'
import { ArchiveIcon } from '../../elements/icons/ArchiveIcon'
import { TrashIcon } from '../../elements/icons/TrashIcon'
import { LabelIcon } from '../../elements/icons/LabelIcon'
import { ListViewIcon } from '../../elements/icons/ListViewIcon'
import { GridViewIcon } from '../../elements/icons/GridViewIcon'
import { CaretDownIcon } from '../../elements/icons/CaretDownIcon'
import { PinnedButtons } from './PinnedButtons'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { PinnedSearch } from '../../../pages/settings/pinned-searches'
import { HeaderCheckboxIcon } from '../../elements/icons/HeaderCheckboxIcon'
import {
HeaderCheckboxCheckedIcon,
HeaderCheckboxIcon,
} from '../../elements/icons/HeaderCheckboxIcon'
import { HeaderSearchIcon } from '../../elements/icons/HeaderSearchIcon'
import { HeaderToggleGridIcon } from '../../elements/icons/HeaderToggleGridIcon'
import { HeaderToggleListIcon } from '../../elements/icons/HeaderToggleListIcon'
@ -134,9 +122,10 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
}}
>
{props.multiSelectMode !== 'off' ? (
<HStack alignment="center" css={{ width: '100% ' }}>
<>
<CheckBoxButton {...props} />
<MultiSelectControls {...props} />
</HStack>
</>
) : (
<HeaderControls {...props} />
)}
@ -144,6 +133,31 @@ function LargeHeaderLayout(props: LibraryHeaderProps): JSX.Element {
)
}
const CheckBoxButton = (props: LibraryHeaderProps): JSX.Element => {
return (
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
switch (props.multiSelectMode) {
case 'off':
case 'none':
case 'some':
props.setMultiSelectMode('visible')
break
default:
props.setMultiSelectMode('off')
break
}
e.preventDefault()
}}
>
<HeaderCheckboxIcon multiSelectMode={props.multiSelectMode} />
</Button>
)
}
const HeaderControls = (props: LibraryHeaderProps): JSX.Element => {
const [showSearchBar, setShowSearchBar] = useState(false)
return (
@ -156,17 +170,7 @@ const HeaderControls = (props: LibraryHeaderProps): JSX.Element => {
>
<MenuHeaderButton {...props} />
</SpanBox>
<Button
title="Select multiple"
style="plainIcon"
css={{ display: 'flex', '&:hover': { opacity: '1.0' } }}
onClick={(e) => {
props.setMultiSelectMode('visible')
e.preventDefault()
}}
>
<HeaderCheckboxIcon />
</Button>
<CheckBoxButton {...props} />
{showSearchBar ? (
<SearchBox {...props} setShowSearchBar={setShowSearchBar} />
@ -441,262 +445,202 @@ type ControlButtonBoxProps = {
function MultiSelectControls(props: ControlButtonBoxProps): JSX.Element {
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [showLabelsModal, setShowLabelsModal] = useState(false)
return (
<HStack alignment="center" distribution="center" css={{ gap: '20px' }}>
<Button
style="outline"
onClick={(e) => {
props.performMultiSelectAction(BulkAction.ARCHIVE)
e.preventDefault()
}}
>
<ArchiveIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Archive</SpanBox>
</Button>
<Button
style="outline"
onClick={(e) => {
setShowLabelsModal(true)
e.preventDefault()
}}
>
<LabelIcon size={20} color={theme.colors.thTextContrast2.toString()} />
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Add Labels</SpanBox>
</Button>
<Button
style="outline"
onClick={(e) => {
setShowConfirmDelete(true)
e.preventDefault()
}}
>
<TrashIcon size={20} color={theme.colors.thTextContrast2.toString()} />
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Delete</SpanBox>
</Button>
<Button
style="cancel"
onClick={(e) => {
props.setMultiSelectMode('off')
e.preventDefault()
}}
>
<Prohibit
width={20}
height={20}
color={theme.colors.thTextContrast2.toString()}
/>
<SpanBox css={{ '@lgDown': { display: 'none' } }}>Cancel</SpanBox>
</Button>
{showConfirmDelete && (
<ConfirmationModal
message={`You are about to delete ${props.numItemsSelected} items. All associated notes and highlights will be deleted.`}
acceptButtonLabel={'Delete'}
onAccept={() => {
props.performMultiSelectAction(BulkAction.DELETE)
}}
onOpenChange={(open: boolean) => {
setShowConfirmDelete(false)
}}
/>
)}
{showLabelsModal && (
<AddBulkLabelsModal
bulkSetLabels={(labels: Label[]) => {
const labelIds = labels.map((l) => l.id)
props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds)
}}
onOpenChange={(open: boolean) => {
setShowLabelsModal(false)
}}
/>
)}
</HStack>
)
}
type SearchControlButtonBoxProps = ControlButtonBoxProps
function SearchControlButtonBox(
props: SearchControlButtonBoxProps
): JSX.Element {
return (
<>
<Button
style="plainIcon"
css={{ display: 'flex', marginLeft: 'auto' }}
onClick={(e) => {
props.updateLayout(
props.layout == 'GRID_LAYOUT' ? 'LIST_LAYOUT' : 'GRID_LAYOUT'
)
e.preventDefault()
}}
>
{props.layout == 'GRID_LAYOUT' ? (
<ListViewIcon size={30} color={'#898989'} />
) : (
<GridViewIcon size={30} color={'#898989'} />
)}
</Button>
</>
)
}
const MuliSelectControl = (props: ControlButtonBoxProps): JSX.Element => {
const [isChecked, setIsChecked] = useState(false)
useEffect(() => {
if (props.multiSelectMode === 'off' || props.multiSelectMode === 'none') {
setIsChecked(false)
}
}, [props.multiSelectMode])
const compact = false
return (
<Box
css={{
display: 'flex',
padding: '10px',
height: '38px',
width: '100%',
maxWidth: '521px',
bg: '$thLibrarySearchbox',
borderRadius: '6px',
boxShadow: '0px 1px 2px 0px rgba(0, 0, 0, 0.05);',
'@mdDown': {
mx: '5px',
},
borderRadius: '100px',
border: '2px solid transparent',
boxShadow:
'0 1px 3px 0 rgba(0, 0, 0, 0.1),0 1px 2px 0 rgba(0, 0, 0, 0.06);',
}}
>
<SpanBox
<HStack
alignment="center"
distribution="end"
css={{
flex: 1,
display: 'flex',
gap: '5px',
alignItems: 'center',
width: '100%',
height: '100%',
gap: '15px',
pl: compact ? '10px' : '15px',
pr: compact ? '5px' : '10px',
}}
onClick={(e) => {
e.preventDefault()
}}
>
<CardCheckbox
isChecked={isChecked}
handleChanged={() => {
const newValue = !isChecked
props.setMultiSelectMode(newValue ? 'visible' : 'off')
setIsChecked(newValue)
<SpanBox
css={{
fontSize: '14px',
fontFamily: '$display',
marginRight: 'auto',
}}
/>
<SpanBox css={{ display: 'flex', pb: '2px' }}>
<Dropdown
triggerElement={
<CaretDownIcon
size={9}
color={theme.colors.graySolid.toString()}
/>
}
>
<DropdownOption
onSelect={() => {
setIsChecked(true)
props.setMultiSelectMode('visible')
}}
title="All"
/>
{/* <DropdownOption
onSelect={() => {
setIsChecked(true)
props.setMultiSelectMode('search')
}}
title="All matching search"
/> */}
</Dropdown>
>
{props.numItemsSelected} items selected
</SpanBox>
</SpanBox>
<Button
title="Archive"
css={{ display: 'flex' }}
style="plainIcon"
onClick={(e) => {
props.performMultiSelectAction(BulkAction.ARCHIVE)
e.preventDefault()
}}
>
<ArchiveIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
<Button
title="Add labels"
css={{ display: 'flex' }}
style="plainIcon"
onClick={(e) => {
setShowLabelsModal(true)
e.preventDefault()
}}
>
<LabelIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
<Button
title="Delete"
css={{ display: 'flex' }}
style="plainIcon"
onClick={(e) => {
setShowConfirmDelete(true)
e.preventDefault()
}}
>
<TrashIcon
size={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
{showConfirmDelete && (
<ConfirmationModal
message={`You are about to delete ${props.numItemsSelected} items. All associated notes and highlights will be deleted.`}
acceptButtonLabel={'Delete'}
onAccept={() => {
props.performMultiSelectAction(BulkAction.DELETE)
}}
onOpenChange={(open: boolean) => {
setShowConfirmDelete(false)
}}
/>
)}
{showLabelsModal && (
<AddBulkLabelsModal
bulkSetLabels={(labels: Label[]) => {
const labelIds = labels.map((l) => l.id)
props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds)
}}
onOpenChange={(open: boolean) => {
setShowLabelsModal(false)
}}
/>
)}
<Button
title="Cancel"
css={{ display: 'flex', mr: '10px' }}
style="plainIcon"
onClick={(event) => {
props.setMultiSelectMode('off')
}}
tabIndex={-1}
>
<X
width={20}
height={20}
color={theme.colors.thTextContrast2.toString()}
/>
</Button>
</HStack>
</Box>
// <HStack
// alignment="center"
// distribution="center"
// css={{ gap: '20px', width: '100% ' }}
// >
// <Button
// style="outline"
// onClick={(e) => {
// props.performMultiSelectAction(BulkAction.ARCHIVE)
// e.preventDefault()
// }}
// >
// <ArchiveIcon
// size={20}
// color={theme.colors.thTextContrast2.toString()}
// />
// <SpanBox css={{ '@lgDown': { display: 'none' } }}>Archive</SpanBox>
// </Button>
// <Button
// style="outline"
// onClick={(e) => {
// setShowLabelsModal(true)
// e.preventDefault()
// }}
// >
// <LabelIcon size={20} color={theme.colors.thTextContrast2.toString()} />
// <SpanBox css={{ '@lgDown': { display: 'none' } }}>Add Labels</SpanBox>
// </Button>
// <Button
// style="outline"
// onClick={(e) => {
// setShowConfirmDelete(true)
// e.preventDefault()
// }}
// >
// <TrashIcon size={20} color={theme.colors.thTextContrast2.toString()} />
// <SpanBox css={{ '@lgDown': { display: 'none' } }}>Delete</SpanBox>
// </Button>
// <Button
// style="cancel"
// onClick={(e) => {
// props.setMultiSelectMode('off')
// e.preventDefault()
// }}
// >
// <Prohibit
// width={20}
// height={20}
// color={theme.colors.thTextContrast2.toString()}
// />
// <SpanBox css={{ '@lgDown': { display: 'none' } }}>Cancel</SpanBox>
// </Button>
// {showConfirmDelete && (
// <ConfirmationModal
// message={`You are about to delete ${props.numItemsSelected} items. All associated notes and highlights will be deleted.`}
// acceptButtonLabel={'Delete'}
// onAccept={() => {
// props.performMultiSelectAction(BulkAction.DELETE)
// }}
// onOpenChange={(open: boolean) => {
// setShowConfirmDelete(false)
// }}
// />
// )}
// {showLabelsModal && (
// <AddBulkLabelsModal
// bulkSetLabels={(labels: Label[]) => {
// const labelIds = labels.map((l) => l.id)
// props.performMultiSelectAction(BulkAction.ADD_LABELS, labelIds)
// }}
// onOpenChange={(open: boolean) => {
// setShowLabelsModal(false)
// }}
// />
// )}
// </HStack>
)
}
// function ControlButtonBox(props: ControlButtonBoxProps): JSX.Element {
// return (
// <>
// <SpanBox
// css={{
// flex: 1,
// display: 'flex',
// gap: '2px',
// alignItems: 'center',
// }}
// >
// <SpanBox
// css={{
// color: '#55B938',
// paddingLeft: '5px',
// fontSize: '12px',
// fontWeight: '600',
// fontFamily: '$inter',
// '@xlgDown': {
// paddingLeft: '5px',
// },
// }}
// >
// {props.numItemsSelected}{' '}
// <SpanBox
// css={{
// '@media (max-width: 1280px)': { display: 'none' },
// }}
// >
// selected
// </SpanBox>
// </SpanBox>
// </SpanBox>
// )}
// {/* {props.multiSelectMode !== 'off' ? (
// <>
// <MultiSelectControls {...props} />
// <SpanBox css={{ flex: 1 }}></SpanBox>
// </>
// ) : (
// <SearchControlButtonBox {...props} />
// )} */}
// {/* </HStack> */}
// // {props.setShowInlineSearch && props.multiSelectMode === 'off' && (
// // <HStack
// // alignment="center"
// // distribution="end"
// // css={{
// // marginLeft: 'auto',
// // marginRight: '20px',
// // width: '100px',
// // height: '100%',
// // gap: '20px',
// // '@md': {
// // display: 'none',
// // },
// // }}
// // >
// // <Button
// // style="ghost"
// // onClick={() => {
// // props.setShowInlineSearch && props.setShowInlineSearch(true)
// // }}
// // css={{
// // display: 'flex',
// // }}
// // >
// // <MagnifyingGlass
// // size={20}
// // color={theme.colors.graySolid.toString()}
// // />
// // </Button>
// // <PrimaryDropdown
// // showThemeSection={true}
// // layout={props.layout}
// // updateLayout={props.updateLayout}
// // />
// // </HStack>
// // )}
// </>
// )
// }