Merge pull request #3540 from omnivore-app/feat/web-new-left-menu

WIP on left menu improvements
This commit is contained in:
Jackson Harper 2024-02-22 10:38:50 +08:00 committed by GitHub
commit 96e3fdb8c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2053 additions and 167 deletions

View file

@ -1,20 +1,13 @@
import { SpanBox, HStack } from './LayoutPrimitives'
import { Circle, X } from 'phosphor-react'
import { isDarkTheme } from '../../lib/themeUpdater'
type LabelChipProps = {
text: string
color: string // expected to be a RGB hex color string
isSelected?: boolean
useAppAppearance?: boolean
}
export function LabelChip(props: LabelChipProps): JSX.Element {
const isDark = isDarkTheme()
const selectedBorder = isDark ? '#FFEA9F' : 'black'
const unSelectedBorder = isDark ? '#2A2A2A' : '#F5F5F5'
return (
<SpanBox
css={{
@ -30,9 +23,11 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
borderRadius: '5px',
borderWidth: '1px',
borderStyle: 'solid',
color: isDark ? '#EBEBEB' : '#2A2A2A',
borderColor: props.isSelected ? selectedBorder : unSelectedBorder,
backgroundColor: isDark ? '#2A2A2A' : '#F5F5F5',
color: '$thLabelChipForeground',
borderColor: props.isSelected
? '$thLabelChipSelectedBorder'
: '$thLabelChipUnselectedBorder',
backgroundColor: '$thLabelChipBackground',
}}
>
<HStack alignment="center" css={{ gap: '5px' }}>

View file

@ -0,0 +1,35 @@
import { SpanBox, HStack } from './LayoutPrimitives'
import { Circle, X } from 'phosphor-react'
type LabelChipProps = {
text: string
color: string // expected to be a RGB hex color string
isSelected?: boolean
}
export function OutlinedLabelChip(props: LabelChipProps): JSX.Element {
return (
<HStack
alignment="center"
distribution="start"
css={{
gap: '10px',
fontSize: '14px',
fontWeight: '500',
fontFamily: '$inter',
padding: '2.5px 12px',
whiteSpace: 'nowrap',
cursor: 'pointer',
backgroundClip: 'padding-box',
borderRadius: '5px',
borderWidth: '1px',
borderStyle: 'solid',
borderColor: '$thLabelOutlineChipBorder',
color: '$thLabelChipForeground',
}}
>
<Circle size={14} color={props.color} weight="fill" />
<SpanBox css={{ pr: '2px' }}>{props.text}</SpanBox>
</HStack>
)
}

View file

@ -37,6 +37,23 @@ const textVariants = {
my: '6px',
color: '$grayText',
},
settingsSection: {
fontWeight: '600',
fontSize: '17px',
fontFamily: '$inter',
color: '$grayText',
m: '0px',
my: '15px',
marginBlockStart: '0px',
marginBlockEnd: '0px',
},
settingsItem: {
fontSize: '13px',
fontFamily: '$display',
color: '$grayText',
marginBlockStart: '0px',
marginBlockEnd: '0px',
},
userNote: {
fontSize: '16px',
paddingTop: '0px',

View file

@ -0,0 +1,73 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class DragIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="21"
height="21"
viewBox="0 0 21 21"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M3.84198 8.00234C3.84198 8.22335 3.92978 8.43531 4.08606 8.5916C4.24234 8.74788 4.4543 8.83567 4.67531 8.83567C4.89633 8.83567 5.10829 8.74788 5.26457 8.5916C5.42085 8.43531 5.50865 8.22335 5.50865 8.00234C5.50865 7.78133 5.42085 7.56936 5.26457 7.41308C5.10829 7.2568 4.89633 7.16901 4.67531 7.16901C4.4543 7.16901 4.24234 7.2568 4.08606 7.41308C3.92978 7.56936 3.84198 7.78133 3.84198 8.00234Z"
fill="#D9D9D9"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M3.84198 13.0023C3.84198 13.2234 3.92978 13.4353 4.08606 13.5916C4.24234 13.7479 4.4543 13.8357 4.67531 13.8357C4.89633 13.8357 5.10829 13.7479 5.26457 13.5916C5.42085 13.4353 5.50865 13.2234 5.50865 13.0023C5.50865 12.7813 5.42085 12.5694 5.26457 12.4131C5.10829 12.2568 4.89633 12.169 4.67531 12.169C4.4543 12.169 4.24234 12.2568 4.08606 12.4131C3.92978 12.5694 3.84198 12.7813 3.84198 13.0023Z"
fill="#D9D9D9"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.67535 8.00234C9.67535 8.22335 9.76315 8.43531 9.91943 8.5916C10.0757 8.74788 10.2877 8.83567 10.5087 8.83567C10.7297 8.83567 10.9417 8.74788 11.0979 8.5916C11.2542 8.43531 11.342 8.22335 11.342 8.00234C11.342 7.78133 11.2542 7.56936 11.0979 7.41308C10.9417 7.2568 10.7297 7.16901 10.5087 7.16901C10.2877 7.16901 10.0757 7.2568 9.91943 7.41308C9.76315 7.56936 9.67535 7.78133 9.67535 8.00234Z"
fill="#D9D9D9"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9.67535 13.0023C9.67535 13.2234 9.76315 13.4353 9.91943 13.5916C10.0757 13.7479 10.2877 13.8357 10.5087 13.8357C10.7297 13.8357 10.9417 13.7479 11.0979 13.5916C11.2542 13.4353 11.342 13.2234 11.342 13.0023C11.342 12.7813 11.2542 12.5694 11.0979 12.4131C10.9417 12.2568 10.7297 12.169 10.5087 12.169C10.2877 12.169 10.0757 12.2568 9.91943 12.4131C9.76315 12.5694 9.67535 12.7813 9.67535 13.0023Z"
fill="#D9D9D9"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.5087 8.00234C15.5087 8.22335 15.5965 8.43531 15.7527 8.5916C15.909 8.74788 16.121 8.83567 16.342 8.83567C16.563 8.83567 16.775 8.74788 16.9313 8.5916C17.0875 8.43531 17.1753 8.22335 17.1753 8.00234C17.1753 7.78133 17.0875 7.56936 16.9313 7.41308C16.775 7.2568 16.563 7.16901 16.342 7.16901C16.121 7.16901 15.909 7.2568 15.7527 7.41308C15.5965 7.56936 15.5087 7.78133 15.5087 8.00234Z"
fill="#D9D9D9"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M15.5087 13.0023C15.5087 13.2234 15.5965 13.4353 15.7527 13.5916C15.909 13.7479 16.121 13.8357 16.342 13.8357C16.563 13.8357 16.775 13.7479 16.9313 13.5916C17.0875 13.4353 17.1753 13.2234 17.1753 13.0023C17.1753 12.7813 17.0875 12.5694 16.9313 12.4131C16.775 12.2568 16.563 12.169 16.342 12.169C16.121 12.169 15.909 12.2568 15.7527 12.4131C15.5965 12.5694 15.5087 12.7813 15.5087 13.0023Z"
fill="#D9D9D9"
stroke="#D9D9D9"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>
)
}
}

View file

@ -22,20 +22,20 @@ export class FollowingIcon extends React.Component<IconProps> {
d="M12.5 4.5L4.5 8.5L12.5 12.5L20.5 8.5L12.5 4.5Z"
fill={color}
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.5 12.5L12.5 16.5L20.5 12.5"
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.5 16.5L12.5 20.5L20.5 16.5"
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>

View file

@ -4,38 +4,48 @@ import { IconProps } from './IconProps'
import React from 'react'
export class FollowingIcon extends React.Component<IconProps> {
export class HighlightsIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width={size}
height={size}
viewBox="0 0 25 25"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M12.5 4.5L4.5 8.5L12.5 12.5L20.5 8.5L12.5 4.5Z"
fill={color}
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
d="M3.74023 18.8567H7.57357L17.6361 8.79423C18.1444 8.2859 18.43 7.59646 18.43 6.87757C18.43 6.15868 18.1444 5.46923 17.6361 4.9609C17.1277 4.45257 16.4383 4.16699 15.7194 4.16699C15.0005 4.16699 14.3111 4.45257 13.8027 4.9609L3.74023 15.0234V18.8567Z"
fill="#FFD234"
stroke="#FFD234"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.5 12.5L12.5 16.5L20.5 12.5"
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
d="M12.8447 5.91931L16.6781 9.75265"
stroke="#FFD234"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M4.5 16.5L12.5 20.5L20.5 16.5"
stroke={color}
stroke-linecap="round"
stroke-linejoin="round"
d="M5.17773 13.5859L9.01107 17.4193"
stroke="#FFD234"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M20.9899 15.0234V18.8568H13.3232L17.1566 15.0234H20.9899Z"
stroke="#FFD234"
strokeWidth="1.25"
strokeLinecap="round"
strokeLinejoin="round"
/>
</g>
</svg>

View file

@ -0,0 +1,29 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class HomeIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="26"
height="26"
viewBox="0 0 26 26"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M19.9329 12.3418L12.7458 6.07813C12.3924 5.77033 11.8662 5.77093 11.514 6.07933L4.35753 12.3433C3.70713 12.9127 4.11003 13.984 4.97433 13.984C5.49123 13.984 5.91033 14.4031 5.91033 14.9203V20.3893C5.91033 20.9065 6.32973 21.3253 6.84663 21.3253H10.1271V16.5322C10.1271 16.2397 10.3638 16.003 10.656 16.003H13.6359C13.9281 16.003 14.1651 16.2397 14.1651 16.5322V21.3256H17.2806C17.7978 21.3256 18.2169 20.9068 18.2169 20.3896V14.9206C18.2169 14.4034 18.636 13.9843 19.1529 13.9843H19.3179C20.1831 13.9837 20.5854 12.9103 19.9329 12.3418Z"
fill={color}
/>
</g>
</svg>
)
}
}

View file

@ -6,9 +6,6 @@ import React from 'react'
export class NewsletterFlairIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 26).toString()
const color = (this.props.color || '#2A2A2A').toString()
return (
<svg
width="17"

View file

@ -0,0 +1,32 @@
/* eslint-disable functional/no-class */
/* eslint-disable functional/no-this-expression */
import { IconProps } from './IconProps'
import React from 'react'
export class NewsletterIcon extends React.Component<IconProps> {
render() {
const size = (this.props.size || 20).toString()
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M14.6666 5.02332V11.3333C14.6666 11.8435 14.4717 12.3343 14.1217 12.7055C13.7718 13.0767 13.2932 13.3001 12.7839 13.33L12.6666 13.3333H3.33325C2.82311 13.3333 2.33224 13.1384 1.96108 12.7885C1.58991 12.4385 1.36651 11.9599 1.33659 11.4506L1.33325 11.3333V5.02332L7.62992 9.22132L7.70725 9.26532C7.79839 9.30984 7.89849 9.33299 7.99992 9.33299C8.10135 9.33299 8.20145 9.30984 8.29259 9.26532L8.36992 9.22132L14.6666 5.02332Z"
fill="#007AFF"
/>
<path
d="M12.6665 2.66669C13.3865 2.66669 14.0179 3.04669 14.3699 3.61802L7.99988 7.86469L1.62988 3.61802C1.79704 3.34653 2.02669 3.11895 2.29969 2.95428C2.57268 2.7896 2.8811 2.69259 3.19922 2.67135L3.33322 2.66669H12.6665Z"
fill="#007AFF"
/>
</g>
</svg>
)
}
}

View file

@ -2,7 +2,6 @@ import { Box, VStack, HStack, SpanBox } from '../../elements/LayoutPrimitives'
import { LabelChip } from '../../elements/LabelChip'
import type { LinkedItemCardProps } from './CardTypes'
import { useCallback, useState } from 'react'
import Link from 'next/link'
import {
AuthorInfoStyle,
CardCheckbox,

View file

@ -1,35 +0,0 @@
import { HStack } from '../elements/LayoutPrimitives'
import { UserBasicData } from '../../lib/networking/queries/useGetViewerQuery'
import { DEFAULT_HEADER_HEIGHT } from '../templates/homeFeed/HeaderSpacer'
import { LogoBox } from '../elements/LogoBox'
type HeaderProps = {
user?: UserBasicData
}
export function SettingsHeader(props: HeaderProps): JSX.Element {
return (
<nav>
<HStack
alignment="center"
distribution="start"
css={{
top: '0',
left: '0',
zIndex: 1,
display: 'flex',
position: 'fixed',
width: '100%',
pr: '25px',
height: DEFAULT_HEADER_HEIGHT,
'@mdDown': {
pr: '15px',
},
bg: '$thBackground',
}}
>
<LogoBox />
</HStack>
</nav>
)
}

View file

@ -218,19 +218,14 @@ export function PrimaryDropdown(props: PrimaryDropdownProps): JSX.Element {
onSelect={() => headerDropdownActionHandler('navigate-to-rules')}
title="Rules"
/>
<DropdownMenu>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
title="API Keys"
/>
<DropdownOption
onSelect={() =>
headerDropdownActionHandler('navigate-to-integrations')
}
title="Integrations"
/>
</DropdownMenu>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-api')}
title="API Keys"
/>
<DropdownOption
onSelect={() => headerDropdownActionHandler('navigate-to-integrations')}
title="Integrations"
/>
<DropdownOption
onSelect={() => window.open('https://docs.omnivore.app', '_blank')}
title="Documentation"

View file

@ -1,6 +1,5 @@
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
import { SettingsHeader } from '../patterns/SettingsHeader'
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
import { useRouter } from 'next/router'
@ -47,7 +46,6 @@ export function SettingsLayout(props: SettingsLayoutProps): JSX.Element {
css={{ width: '100%', height: '100%', minHeight: '100vh' }}
>
<PageMetaData path="settings" title="Settings" />
<SettingsHeader user={viewerData?.me} />
<VStack css={{ width: '100%', height: '100%' }}>
<Box
css={{

View file

@ -435,7 +435,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
key={label.id}
text={label.name}
color={label.color}
useAppAppearance={props.isAppleAppEmbed}
/>
))}
</SpanBox>

View file

@ -12,6 +12,7 @@ import type { Highlight } from '../../../lib/networking/fragments/highlightFragm
import {
getHighlightElements,
highlightIdAttribute,
highlightLabelIdAttribute,
highlightNoteIdAttribute,
SelectionAttributes,
} from '../../../lib/highlights/highlightHelpers'
@ -79,15 +80,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
const focusedHighlightMousePos = useRef({ pageX: 0, pageY: 0 })
const [currentHighlightIdx, setCurrentHighlightIdx] = useState(0)
const [focusedHighlight, setFocusedHighlight] = useState<
Highlight | undefined
>(undefined)
const [focusedHighlight, setFocusedHighlight] =
useState<Highlight | undefined>(undefined)
const [selectionData, setSelectionData] = useSelection(highlightLocations)
const [labelsTarget, setLabelsTarget] = useState<Highlight | undefined>(
undefined
)
const [labelsTarget, setLabelsTarget] =
useState<Highlight | undefined>(undefined)
const [
confirmDeleteHighlightWithNoteId,
@ -388,6 +387,13 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
highlight: highlight,
highlightModalAction: 'addComment',
})
} else if ((target as Element).hasAttribute(highlightLabelIdAttribute)) {
const id = (target as HTMLSpanElement).getAttribute(
highlightLabelIdAttribute
)
const highlight = highlights.find(($0) => $0.id === id)
setFocusedHighlight(highlight)
setLabelsTarget(highlight)
} else {
window?.webkit?.messageHandlers.viewerAction?.postMessage({
actionID: 'pageTapped',
@ -395,7 +401,7 @@ export function HighlightsLayer(props: HighlightsLayerProps): JSX.Element {
setFocusedHighlight(undefined)
}
},
[openNoteModal, highlights]
[openNoteModal, highlights, setLabelsTarget]
)
const handleDoubleClick = useCallback(

View file

@ -20,23 +20,24 @@ import {
import { LibraryHighlightGridCard } from '../../patterns/LibraryCards/LibraryHighlightGridCard'
import { NotebookContent } from '../article/Notebook'
import { EmptyHighlights } from './EmptyHighlights'
import { DEFAULT_HEADER_HEIGHT } from './HeaderSpacer'
import { highlightsAsMarkdown } from './HighlightItem'
import { MenuHeaderButton } from './LibraryHeader'
type HighlightItemsLayoutProps = {
items: LibraryItem[]
viewer: UserBasicData | undefined
showFilterMenu: boolean
setShowFilterMenu: (show: boolean) => void
gridContainerRef: React.RefObject<HTMLDivElement>
}
export function HighlightItemsLayout(
props: HighlightItemsLayoutProps
): JSX.Element {
// const headerHeight = useGetHeaderHeight()
const [currentItem, setCurrentItem] = useState<LibraryItem | undefined>(
undefined
)
const [currentItem, setCurrentItem] =
useState<LibraryItem | undefined>(undefined)
const listReducer = (
state: LibraryItem[],
@ -106,10 +107,7 @@ export function HighlightItemsLayout(
<Box
css={{
width: '100%',
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
'@mdDown': {
height: DEFAULT_HEADER_HEIGHT,
},
height: `100vh`,
}}
>
<EmptyHighlights />
@ -122,15 +120,12 @@ export function HighlightItemsLayout(
<HStack
css={{
width: '100%',
height: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
height: `100vh`,
'@lgDown': {
overflowY: 'scroll',
},
bg: '$thBackground2',
overflow: 'hidden',
'@mdDown': {
height: DEFAULT_HEADER_HEIGHT,
},
}}
distribution="start"
alignment="start"
@ -153,7 +148,7 @@ export function HighlightItemsLayout(
>
<VStack
css={{
minHeight: `calc(100vh - ${DEFAULT_HEADER_HEIGHT})`,
minHeight: `100vh`,
bg: '$thBackground',
}}
distribution="start"
@ -167,8 +162,22 @@ export function HighlightItemsLayout(
borderBottom: '1px solid $thBorderColor',
}}
alignment="center"
distribution="center"
></HStack>
distribution="start"
>
<SpanBox
css={{
display: 'none',
'@mdDown': {
display: 'flex',
},
}}
>
<MenuHeaderButton
showFilterMenu={props.showFilterMenu}
setShowFilterMenu={props.setShowFilterMenu}
/>
</SpanBox>
</HStack>
<LibraryItemsList
items={items}
viewer={props.viewer}

View file

@ -37,6 +37,7 @@ import { EditLibraryItemModal } from './EditItemModals'
import { EmptyLibrary } from './EmptyLibrary'
import { HighlightItemsLayout } from './HighlightsLayout'
import { LibraryFilterMenu } from '../navMenu/LibraryMenu'
import { LibraryLegacyMenu } from '../navMenu/LibraryLegacyMenu'
import { LibraryHeader, MultiSelectMode } from './LibraryHeader'
import { UploadModal } from '../UploadModal'
import { BulkAction } from '../../../lib/networking/mutations/bulkActionMutation'
@ -68,7 +69,7 @@ const debouncedFetchSearchResults = debounce((query, cb) => {
// We set a relatively high delay for the refresh at the end, as it's likely there's an issue
// in processing. We give it the best attempt to be able to resolve, but if it doesn't we set
// the state as Failed. On refresh it will try again if the backend sends "PROCESSING"
const TIMEOUT_DELAYS = [1000, 2000, 2500, 3500, 5000, 10000, 60000]
const TIMEOUT_DELAYS = [2000, 3500, 5000]
export function HomeFeedContainer(): JSX.Element {
const { viewerData } = useGetViewerQuery()
@ -85,11 +86,13 @@ export function HomeFeedContainer(): JSX.Element {
const gridContainerRef = useRef<HTMLDivElement>(null)
const [labelsTarget, setLabelsTarget] =
useState<LibraryItem | undefined>(undefined)
const [labelsTarget, setLabelsTarget] = useState<LibraryItem | undefined>(
undefined
)
const [notebookTarget, setNotebookTarget] =
useState<LibraryItem | undefined>(undefined)
const [notebookTarget, setNotebookTarget] = useState<LibraryItem | undefined>(
undefined
)
const [showAddLinkModal, setShowAddLinkModal] = useState(false)
const [showEditTitleModal, setShowEditTitleModal] = useState(false)
@ -206,8 +209,13 @@ export function HomeFeedContainer(): JSX.Element {
let startIdx = 0
const seeIfUpdated = async () => {
if (startIdx > TIMEOUT_DELAYS.length) {
if (startIdx >= TIMEOUT_DELAYS.length) {
item.node.state = State.FAILED
const updatedArticle = { ...item }
updatedArticle.node = { ...item.node }
updatedArticle.isLoading = false
console.log(`Updating Metadata of ${item.node.slug}.`)
performActionOnItem('update-item', updatedArticle)
return
}
@ -931,6 +939,10 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
key: 'libraryLayout',
initialValue: 'LIST_LAYOUT',
})
const [navMenuStyle] = usePersistedState<'legacy' | 'shortcuts'>({
key: 'library-nav-menu-style',
initialValue: 'legacy',
})
const updateLayout = useCallback(
async (newLayout: LayoutType) => {
@ -967,21 +979,35 @@ function HomeFeedGrid(props: HomeFeedContentProps): JSX.Element {
)}
<HStack css={{ width: '100%', height: '100%' }}>
<LibraryFilterMenu
setShowAddLinkModal={props.setShowAddLinkModal}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
/>
{navMenuStyle == 'shortcuts' && (
<LibraryFilterMenu
setShowAddLinkModal={props.setShowAddLinkModal}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
/>
)}
{navMenuStyle == 'legacy' && (
<LibraryLegacyMenu
setShowAddLinkModal={props.setShowAddLinkModal}
searchTerm={props.searchTerm}
applySearchQuery={(searchQuery: string) => {
props.applySearchQuery(searchQuery)
}}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
/>
)}
{!props.isValidating && props.mode == 'highlights' && (
<HighlightItemsLayout
gridContainerRef={props.gridContainerRef}
items={props.items}
viewer={viewerData?.me}
showFilterMenu={showFilterMenu}
setShowFilterMenu={setShowFilterMenu}
/>
)}

View file

@ -48,7 +48,8 @@ export const headerControlWidths = (
return {
width: '95%',
'@mdDown': {
width: multiSelectMode !== 'off' ? '100%' : '95%',
padding: '15px',
width: '100%',
},
'@media (min-width: 930px)': {
width: '620px',

View file

@ -0,0 +1,637 @@
import { ReactNode, useEffect, useMemo, useRef } from 'react'
import { StyledText } from '../../elements/StyledText'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { Circle, X } from 'phosphor-react'
import {
Subscription,
SubscriptionType,
useGetSubscriptionsQuery,
} from '../../../lib/networking/queries/useGetSubscriptionsQuery'
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
import { Label } from '../../../lib/networking/fragments/labelFragment'
import { theme } from '../../tokens/stitches.config'
import { useRegisterActions } from 'kbar'
import { LogoBox } from '../../elements/LogoBox'
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
import { useGetSavedSearchQuery } from '../../../lib/networking/queries/useGetSavedSearchQuery'
import { SavedSearch } from '../../../lib/networking/fragments/savedSearchFragment'
import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
import Link from 'next/link'
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
import { NavMenuFooter } from './Footer'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
type LibraryFilterMenuProps = {
setShowAddLinkModal: (show: boolean) => void
searchTerm: string | undefined
applySearchQuery: (searchTerm: string) => void
showFilterMenu: boolean
setShowFilterMenu: (show: boolean) => void
}
export function LibraryLegacyMenu(props: LibraryFilterMenuProps): JSX.Element {
const [labels, setLabels] = usePersistedState<Label[]>({
key: 'menu-labels',
isSessionStorage: false,
initialValue: [],
})
const [savedSearches, setSavedSearches] = usePersistedState<SavedSearch[]>({
key: 'menu-searches',
isSessionStorage: false,
initialValue: [],
})
const [subscriptions, setSubscriptions] = usePersistedState<Subscription[]>({
key: 'menu-subscriptions',
isSessionStorage: false,
initialValue: [],
})
const labelsResponse = useGetLabelsQuery()
const searchesResponse = useGetSavedSearchQuery()
const subscriptionsResponse = useGetSubscriptionsQuery()
useEffect(() => {
if (
!labelsResponse.error &&
!labelsResponse.isLoading &&
labelsResponse.labels
) {
setLabels(labelsResponse.labels)
}
}, [setLabels, labelsResponse])
useEffect(() => {
if (
!subscriptionsResponse.error &&
!subscriptionsResponse.isLoading &&
subscriptionsResponse.subscriptions
) {
setSubscriptions(subscriptionsResponse.subscriptions)
}
}, [setSubscriptions, subscriptionsResponse])
useEffect(() => {
if (
!searchesResponse.error &&
!searchesResponse.isLoading &&
searchesResponse.savedSearches
) {
setSavedSearches(searchesResponse.savedSearches)
}
}, [setSavedSearches, searchesResponse])
return (
<>
<Box
css={{
left: '0px',
top: '0px',
position: 'fixed',
bg: '$thLeftMenuBackground',
height: '100%',
width: LIBRARY_LEFT_MENU_WIDTH,
overflowY: 'auto',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
display: 'none',
},
'@mdDown': {
width: '100%',
transition: 'top 100ms, visibility 100ms',
top: props.showFilterMenu ? '0' : '100%',
visibility: props.showFilterMenu ? 'visible' : 'hidden',
},
zIndex: 10,
}}
>
<Box
css={{
width: '100%',
px: '25px',
pb: '17px',
pt: '4.5px',
lineHeight: '1',
'@mdDown': {
pb: '0px',
pt: '5px',
px: '15px',
},
}}
>
<SpanBox css={{ '@mdDown': { display: 'none' } }}>
<LogoBox />
</SpanBox>
<Button
style="plainIcon"
css={{
display: 'none',
'@mdDown': {
display: 'flex',
ml: 'auto',
pt: '10px',
pb: '0px',
},
}}
onClick={(event) => {
props.setShowFilterMenu(false)
event.preventDefault()
}}
>
<X size={30} />
</Button>
</Box>
<SavedSearches {...props} savedSearches={savedSearches} />
<Subscriptions {...props} subscriptions={subscriptions} />
<Labels {...props} labels={labels} />
<NavMenuFooter {...props} />
<Box css={{ height: '250px ' }} />
</Box>
{/* This spacer pushes library content to the right of
the fixed left side menu. */}
<Box
css={{
minWidth: LIBRARY_LEFT_MENU_WIDTH,
height: '100%',
bg: '$thBackground',
'@mdDown': {
display: 'none',
},
}}
></Box>
</>
)
}
function SavedSearches(
props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined }
): JSX.Element {
const sortedSearches = useMemo(() => {
return props.savedSearches
?.filter((it) => it.visible)
?.sort(
(left: SavedSearch, right: SavedSearch) =>
left.position - right.position
)
}, [props.savedSearches])
useRegisterActions(
(sortedSearches ?? []).map((item, idx) => {
const key = String(idx + 1)
return {
id: `saved_search_${key}`,
name: item.name,
shortcut: [key],
section: 'Saved Searches',
keywords: '?' + item.name,
perform: () => {
props.applySearchQuery(item.filter)
},
}
}),
[props.savedSearches]
)
const [collapsed, setCollapsed] = usePersistedState<boolean>({
key: `--saved-searches-collapsed`,
initialValue: false,
})
return (
<MenuPanel
title="Saved Searches"
collapsed={collapsed}
setCollapsed={setCollapsed}
>
{!collapsed &&
sortedSearches &&
sortedSearches?.map((item) => (
<FilterButton
key={item.name}
text={item.name}
filterTerm={item.filter}
{...props}
/>
))}
{!collapsed && sortedSearches !== undefined && (
<EditButton
title="Edit Saved Searches"
destination="/settings/saved-searches"
/>
)}
<Box css={{ height: '10px' }}></Box>
</MenuPanel>
)
}
function Subscriptions(
props: LibraryFilterMenuProps & { subscriptions: Subscription[] | undefined }
): JSX.Element {
const [collapsed, setCollapsed] = usePersistedState<boolean>({
key: `--subscriptions-collapsed`,
initialValue: false,
})
const sortedSubscriptions = useMemo(() => {
if (!props.subscriptions) {
return []
}
return props.subscriptions
.filter((s) => s.status == 'ACTIVE')
.sort((a, b) => a.name.localeCompare(b.name))
}, [props.subscriptions])
useRegisterActions(
(sortedSubscriptions ?? []).map((subscription, idx) => {
const key = String(idx + 1)
const name = subscription.name
return {
id: `subscription_${key}`,
section: 'Subscriptions',
name: name,
keywords: '*' + name,
perform: () => {
props.applySearchQuery(`subscription:\"${name}\"`)
},
}
}),
[sortedSubscriptions]
)
return (
<MenuPanel
title="Subscriptions"
collapsed={collapsed}
setCollapsed={setCollapsed}
>
{!collapsed ? (
<>
<FilterButton
filterTerm="in:inbox has:subscriptions"
text="All"
{...props}
/>
<FilterButton
filterTerm={`in:inbox label:RSS`}
text="Feeds"
{...props}
/>
<FilterButton
filterTerm={`in:inbox label:Newsletter`}
text="Newsletters"
{...props}
/>
{(sortedSubscriptions ?? []).map((item) => {
switch (item.type) {
case SubscriptionType.NEWSLETTER:
return (
<FilterButton
key={item.id}
filterTerm={`in:inbox subscription:\"${item.name}\"`}
text={item.name}
{...props}
/>
)
case SubscriptionType.RSS:
return (
<FilterButton
key={item.id}
filterTerm={`in:inbox rss:\"${item.url}\"`}
text={item.name}
{...props}
/>
)
}
})}
<EditButton
title="Edit Subscriptions"
destination="/settings/subscriptions"
/>
</>
) : (
<SpanBox css={{ mb: '10px' }} />
)}
</MenuPanel>
)
}
function Labels(
props: LibraryFilterMenuProps & { labels: Label[] }
): JSX.Element {
const [collapsed, setCollapsed] = usePersistedState<boolean>({
key: `--labels-collapsed`,
initialValue: false,
})
const sortedLabels = useMemo(() => {
return props.labels.sort((left: Label, right: Label) =>
left.name.localeCompare(right.name)
)
}, [props.labels])
return (
<MenuPanel
title="Labels"
editTitle="Edit Labels"
hideBottomBorder={true}
collapsed={collapsed}
setCollapsed={setCollapsed}
>
{!collapsed && (
<>
{sortedLabels.map((item) => {
return <LabelButton key={item.id} label={item} {...props} />
})}
<EditButton title="Edit Labels" destination="/settings/labels" />
</>
)}
</MenuPanel>
)
}
type MenuPanelProps = {
title: string
children: ReactNode
editFunc?: () => void
editTitle?: string
hideBottomBorder?: boolean
collapsed: boolean
setCollapsed: (collapsed: boolean) => void
}
function MenuPanel(props: MenuPanelProps): JSX.Element {
return (
<VStack
css={{
m: '0px',
width: '100%',
borderBottom: props.hideBottomBorder
? '1px solid transparent'
: '1px solid $thBorderColor',
px: '15px',
}}
alignment="start"
distribution="start"
>
<HStack css={{ width: '100%' }} distribution="start" alignment="center">
<StyledText
css={{
fontFamily: 'Inter',
fontWeight: '600',
fontSize: '16px',
lineHeight: '125%',
color: '$thLibraryMenuPrimary',
pl: '10px',
mt: '20px',
mb: '10px',
}}
>
{props.title}
</StyledText>
<SpanBox
css={{
display: 'flex',
height: '100%',
mt: '10px',
marginLeft: 'auto',
verticalAlign: 'middle',
}}
>
<Button
style="articleActionIcon"
onClick={(event) => {
props.setCollapsed(!props.collapsed)
event.preventDefault()
}}
>
{props.collapsed ? (
<ToggleCaretRightIcon
size={15}
color={theme.colors.thLibraryMenuPrimary.toString()}
/>
) : (
<ToggleCaretDownIcon
size={15}
color={theme.colors.thLibraryMenuPrimary.toString()}
/>
)}
</Button>
</SpanBox>
</HStack>
{props.children}
</VStack>
)
}
type FilterButtonProps = {
text: string
filterTerm: string
searchTerm: string | undefined
applySearchQuery: (searchTerm: string) => void
setShowFilterMenu: (show: boolean) => void
}
function FilterButton(props: FilterButtonProps): JSX.Element {
const isInboxFilter = (filter: string) => {
return filter === '' || filter === 'in:inbox'
}
const selected = useMemo(() => {
if (isInboxFilter(props.filterTerm) && !props.searchTerm) {
return true
}
return props.searchTerm === props.filterTerm
}, [props.searchTerm, props.filterTerm])
return (
<Box
css={{
pl: '10px',
mb: '2px',
display: 'flex',
width: '100%',
maxWidth: '100%',
height: '32px',
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color: selected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
alignItems: 'center',
'&:hover': {
backgroundColor: selected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:active': {
backgroundColor: selected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
}}
title={props.text}
onClick={(e) => {
props.applySearchQuery(props.filterTerm)
props.setShowFilterMenu(false)
e.preventDefault()
}}
>
{props.text}
</Box>
)
}
type LabelButtonProps = {
label: Label
searchTerm: string | undefined
applySearchQuery: (searchTerm: string) => void
}
function LabelButton(props: LabelButtonProps): JSX.Element {
const labelId = `checkbox-label-${props.label.id}`
const checkboxRef = useRef<HTMLInputElement | null>(null)
const state = useMemo(() => {
const term = props.searchTerm ?? ''
if (term.indexOf(`label:\"${props.label.name}\"`) >= 0) {
return 'on'
}
return 'off'
}, [props.searchTerm, props.label])
return (
<HStack
css={{
pl: '10px',
pt: '2px', // TODO: hack to middle align
width: '100%',
height: '30px',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color:
state == 'on'
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
m: '0px',
'&:hover': {
backgroundColor: '$thBackground4',
},
}}
title={props.label.name}
alignment="center"
distribution="start"
>
<label
style={{
cursor: 'pointer',
width: '100%',
maxWidth: '170px',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
onClick={() => {
const query = props.searchTerm?.replace(/label:\"(.*)\"/, '') ?? ''
if (checkboxRef.current?.checked) {
props.applySearchQuery(query.trim())
} else {
props.applySearchQuery(
`${query.trim()} label:\"${props.label.name}\"`
)
}
}}
>
<Circle size={9} color={props.label.color} weight="fill" />
<SpanBox css={{ pl: '10px' }}>{props.label.name}</SpanBox>
</label>
<SpanBox
css={{
ml: 'auto',
}}
>
<input
id={labelId}
ref={checkboxRef}
type="checkbox"
checked={state === 'on'}
onChange={(e) => {
if (e.target.checked) {
props.applySearchQuery(
`${props.searchTerm ?? ''} label:\"${props.label.name}\"`
)
} else {
const query =
props.searchTerm?.replace(
`label:\"${props.label.name}\"`,
''
) ?? ''
props.applySearchQuery(query)
}
}}
/>
</SpanBox>
</HStack>
)
}
type EditButtonProps = {
title: string
destination: string
}
function EditButton(props: EditButtonProps): JSX.Element {
return (
<Link href={props.destination} passHref legacyBehavior>
<SpanBox
css={{
ml: '10px',
mb: '10px',
display: 'flex',
alignItems: 'center',
gap: '2px',
'&:hover': {
textDecoration: 'underline',
},
width: '100%',
maxWidth: '100%',
height: '32px',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{props.title}
</SpanBox>
</Link>
)
}

View file

@ -2,7 +2,7 @@ import { ReactNode, useEffect, useMemo, useRef } from 'react'
import { StyledText } from '../../elements/StyledText'
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { Circle } from 'phosphor-react'
import { Circle, DotsThree, MagnifyingGlass, X } from 'phosphor-react'
import {
Subscription,
SubscriptionType,
@ -20,6 +20,19 @@ import { ToggleCaretDownIcon } from '../../elements/icons/ToggleCaretDownIcon'
import Link from 'next/link'
import { ToggleCaretRightIcon } from '../../elements/icons/ToggleCaretRightIcon'
import { NavMenuFooter } from './Footer'
import { FollowingIcon } from '../../elements/icons/FollowingIcon'
import { HomeIcon } from '../../elements/icons/HomeIcon'
import { LibraryIcon } from '../../elements/icons/LibraryIcon'
import { HighlightsIcon } from '../../elements/icons/HighlightsIcon'
import { CoverImage } from '../../elements/CoverImage'
import { Shortcut } from '../../../pages/settings/shortcuts'
import { OutlinedLabelChip } from '../../elements/OutlinedLabelChip'
import { NewsletterFlairIcon } from '../../elements/icons/NewsletterFlairIcon'
import { FeedFlairIcon } from '../../elements/icons/FeedFlairIcon'
import { NewsletterIcon } from '../../elements/icons/NewsletterIcon'
import { DropdownMenu } from '@radix-ui/react-dropdown-menu'
import { Dropdown, DropdownOption } from '../../elements/DropdownElements'
import { useRouter } from 'next/router'
export const LIBRARY_LEFT_MENU_WIDTH = '275px'
@ -99,27 +112,56 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
display: 'none',
},
'@mdDown': {
visibility: props.showFilterMenu ? 'visible' : 'hidden',
width: '100%',
transition: 'visibility 0s, top 150ms',
transition: 'top 100ms, visibility 100ms',
top: props.showFilterMenu ? '0' : '100%',
visibility: props.showFilterMenu ? 'visible' : 'hidden',
},
zIndex: 3,
zIndex: 10,
}}
>
<Box
css={{
width: '100%',
px: '25px',
pb: '25px',
pb: '17px',
pt: '4.5px',
lineHeight: '1',
'@mdDown': {
pb: '0px',
pt: '5px',
px: '15px',
},
}}
>
<LogoBox />
<SpanBox css={{ '@mdDown': { display: 'none' } }}>
<LogoBox />
</SpanBox>
<Button
style="plainIcon"
css={{
display: 'none',
'@mdDown': {
display: 'flex',
ml: 'auto',
pt: '10px',
pb: '0px',
},
}}
onClick={(event) => {
props.setShowFilterMenu(false)
event.preventDefault()
}}
>
<X size={30} />
</Button>
</Box>
<SavedSearches {...props} savedSearches={savedSearches} />
<LibraryNav {...props} />
<Shortcuts {...props} />
{/* <SavedSearches {...props} savedSearches={savedSearches} />
<Subscriptions {...props} subscriptions={subscriptions} />
<Labels {...props} labels={labels} />
<Labels {...props} labels={labels} /> */}
<NavMenuFooter {...props} />
<Box css={{ height: '250px ' }} />
</Box>
@ -139,6 +181,277 @@ export function LibraryFilterMenu(props: LibraryFilterMenuProps): JSX.Element {
)
}
const LibraryNav = (props: LibraryFilterMenuProps): JSX.Element => {
return (
<VStack
css={{
m: '0px',
mt: '10px',
gap: '5px',
width: '100%',
borderBottom: '1px solid $thBorderColor',
px: '15px',
pb: '25px',
}}
alignment="start"
distribution="start"
>
<NavButton
{...props}
text="Home"
filterTerm="in:library OR in:following use:folders"
icon={<HomeIcon color={theme.colors.thHomeIcon.toString()} />}
/>
<NavButton
{...props}
text="Following"
filterTerm="in:following use:folders"
icon={<FollowingIcon color="#F59932" />}
/>
<NavButton
{...props}
text="Library"
filterTerm="in:library use:folders"
icon={<LibraryIcon color={theme.colors.ctaBlue.toString()} />}
/>
<NavButton
{...props}
text="Highlights"
filterTerm="in:all has:highlights mode:highlights"
icon={<HighlightsIcon color={theme.colors.highlight.toString()} />}
/>
</VStack>
)
}
const Shortcuts = (props: LibraryFilterMenuProps): JSX.Element => {
const router = useRouter()
const [shortcuts] = usePersistedState<Shortcut[]>({
key: 'library-shortcuts',
isSessionStorage: false,
initialValue: [],
})
// const shortcuts: Shortcut[] = [
// {
// id: '12asdfasdf',
// name: 'Omnivore Blog',
// icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
// filter: 'subscription:"Money Talk"',
// type: 'feed',
// },
// {
// id: 'sdfsdfgdsfg',
// name: 'Follow the Money | Arne & Harr',
// filter: 'subscription:"Money Talk"',
// type: 'feed',
// },
// {
// id: 'sdfasdfasdfsdfsdfsgasdfg',
// name: 'Andrew Kenneson from Center for the Study of Partisanship and Ideology',
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
// filter: 'in:all label:"Hockey"',
// type: 'newsletter',
// },
// {
// id: 'sdfasdfasdfsdfsdfsgasdfg',
// name: 'Robert的博客',
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
// filter: 'in:all label:"Hockey"',
// type: 'feed',
// },
// {
// id: 'sdfasdfasdfasdfasf',
// name: 'Oldest First',
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
// filter: 'in:all label:"Hockey"',
// type: 'search',
// },
// {
// id: 'sdfasdfasdfgasdfg',
// name: 'Hockey',
// // icon: 'https://substackcdn.com/image/fetch/w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F052c15c4-ecfd-4d32-87db-13bcac9afad5_512x512.png',
// filter: 'in:all label:"Hockey"',
// type: 'label',
// label: {
// id: 'sdfsdfsdf',
// name: 'Hockey',
// color: '#E98B8B',
// createdAt: new Date(),
// },
// },
// ]
//
return (
<VStack
css={{
m: '0px',
gap: '8px',
width: '100%',
px: '15px',
pb: '25px',
}}
alignment="start"
distribution="start"
>
<HStack
alignment="center"
distribution="start"
css={{ width: '100%', pr: '0px' }}
>
<StyledText
css={{
fontFamily: '$display',
fontSize: '14px',
lineHeight: '125%',
color: '$thLibraryMenuPrimary',
pl: '10px',
// mt: '20px',
mb: '10px',
}}
>
SHORTCUTS
</StyledText>
<SpanBox css={{ display: 'flex', ml: 'auto' }}>
<Dropdown
side="bottom"
triggerElement={<DotsThree size={20} />}
css={{ ml: 'auto' }}
>
<DropdownOption
onSelect={() => {
router.push(`/settings/shortcuts`)
}}
title="Edit shortcuts"
/>
</Dropdown>
</SpanBox>
</HStack>
{shortcuts.map((shortcut) => {
const selected = props.searchTerm === shortcut.filter
return (
<Box
key={`shortcut-${shortcut.id}`}
css={{
display: 'flex',
width: '100%',
maxWidth: '100%',
height: '32px',
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
color: selected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'&:hover': {
backgroundColor: selected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:active': {
backgroundColor: selected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
}}
title={shortcut.name}
onClick={(e) => {
props.applySearchQuery(shortcut.filter)
props.setShowFilterMenu(false)
e.preventDefault()
}}
>
{(shortcut.type == 'feed' || shortcut.type == 'newsletter') && (
<FeedOrNewsletterShortcut shortcut={shortcut} />
)}
{shortcut.type == 'search' && (
<SearchShortcut shortcut={shortcut} />
)}
{shortcut.type == 'label' && <LabelShortcut shortcut={shortcut} />}
</Box>
)
})}
</VStack>
)
}
type ShortcutItemProps = {
shortcut: Shortcut
}
const FeedOrNewsletterShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '10px', width: '100%', gap: '10px' }}
key={`search-${props.shortcut.id}`}
>
<HStack
distribution="start"
alignment="center"
css={{ minWidth: '20px' }}
>
{props.shortcut.icon ? (
<CoverImage
src={props.shortcut.icon}
width={20}
height={20}
css={{ borderRadius: '20px' }}
/>
) : props.shortcut.type == 'newsletter' ? (
<NewsletterIcon color="#F59932" size={18} />
) : (
<FollowingIcon color="#F59932" size={21} />
)}
</HStack>
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
</HStack>
)
}
const SearchShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '10px', width: '100%', gap: '10px' }}
key={`search-${props.shortcut.id}`}
>
<HStack
distribution="start"
alignment="center"
css={{ minWidth: '20px' }}
>
<MagnifyingGlass size={18} />
</HStack>
<StyledText style="settingsItem">{props.shortcut.name}</StyledText>
</HStack>
)
}
const LabelShortcut = (props: ShortcutItemProps): JSX.Element => {
return (
<HStack
alignment="center"
distribution="start"
css={{ pl: '5px', width: '100%' }}
key={`search-${props.shortcut.id}`}
>
<OutlinedLabelChip
text={props.shortcut.name}
color={props.shortcut.label?.color ?? 'gray'}
/>
</HStack>
)
}
function SavedSearches(
props: LibraryFilterMenuProps & { savedSearches: SavedSearch[] | undefined }
): JSX.Element {
@ -175,7 +488,7 @@ function SavedSearches(
return (
<MenuPanel
title="Saved Searches"
title="SHORTCUTS"
collapsed={collapsed}
setCollapsed={setCollapsed}
>
@ -353,9 +666,8 @@ function MenuPanel(props: MenuPanelProps): JSX.Element {
<HStack css={{ width: '100%' }} distribution="start" alignment="center">
<StyledText
css={{
fontFamily: 'Inter',
fontWeight: '600',
fontSize: '16px',
fontFamily: '$display',
fontSize: '14px',
lineHeight: '125%',
color: '$thLibraryMenuPrimary',
pl: '10px',
@ -400,6 +712,78 @@ function MenuPanel(props: MenuPanelProps): JSX.Element {
)
}
type NavButtonProps = {
text: string
icon: ReactNode
filterTerm: string
searchTerm: string | undefined
applySearchQuery: (searchTerm: string) => void
setShowFilterMenu: (show: boolean) => void
}
function NavButton(props: NavButtonProps): JSX.Element {
const isInboxFilter = (filter: string) => {
return filter === '' || filter === 'in:inbox'
}
const selected = useMemo(() => {
if (isInboxFilter(props.filterTerm) && !props.searchTerm) {
return true
}
return props.searchTerm === props.filterTerm
}, [props.searchTerm, props.filterTerm])
return (
<HStack
alignment="center"
distribution="start"
css={{
pl: '10px',
mb: '2px',
gap: '10px',
display: 'flex',
width: '100%',
maxWidth: '100%',
height: '34px',
backgroundColor: selected ? '$thLibrarySelectionColor' : 'unset',
fontSize: '15px',
fontWeight: 'regular',
fontFamily: '$display',
color: selected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
'&:hover': {
backgroundColor: selected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
'&:active': {
backgroundColor: selected
? '$thLibrarySelectionColor'
: '$thBackground4',
},
}}
title={props.text}
onClick={(e) => {
props.applySearchQuery(props.filterTerm)
props.setShowFilterMenu(false)
e.preventDefault()
}}
>
{props.icon}
{props.text}
</HStack>
)
}
type FilterButtonProps = {
text: string

View file

@ -12,8 +12,9 @@ import { NavMenuFooter } from './Footer'
const HorizontalDivider = styled(SpanBox, {
width: '100%',
height: '1px',
my: '25px',
background: `${theme.colors.grayLine.toString()}`,
my: '5px',
opacity: '0.1',
background: `${theme.colors.thTextSubtle3.toString()}`,
})
const StyledLink = styled(SpanBox, {
@ -87,6 +88,7 @@ export function SettingsMenu(): JSX.Element {
{ name: 'Feeds', destination: '/settings/feeds' },
{ name: 'Subscriptions', destination: '/settings/subscriptions' },
{ name: 'Labels', destination: '/settings/labels' },
{ name: 'Shortcuts', destination: '/settings/shortcuts' },
{ name: 'Saved Searches', destination: '/settings/saved-searches' },
{ name: 'Pinned Searches', destination: '/settings/pinned-searches' },
]

View file

@ -2,12 +2,38 @@ import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
import { Button } from '../../elements/Button'
import { PrimaryDropdown } from '../PrimaryDropdown'
import { LogoBox } from '../../elements/LogoBox'
import { ReactNode } from 'react'
import { ReactNode, useEffect, useState } from 'react'
import { DEFAULT_HEADER_HEIGHT } from '../homeFeed/HeaderSpacer'
import { theme } from '../../tokens/stitches.config'
import { ReaderSettingsIcon } from '../../elements/icons/ReaderSettingsIcon'
import { CircleUtilityMenuIcon } from '../../elements/icons/CircleUtilityMenuIcon'
function useScrollDirection() {
const [scrollDirection, setScrollDirection] = useState('up')
useEffect(() => {
let lastScrollY = window.pageYOffset
const updateScrollDirection = () => {
const scrollY = window.pageYOffset
const direction = scrollY > lastScrollY ? 'down' : 'up'
if (
direction !== scrollDirection &&
(scrollY - lastScrollY > 10 || scrollY - lastScrollY < -10)
) {
setScrollDirection(direction)
}
lastScrollY = scrollY > 0 ? scrollY : 0
}
window.addEventListener('scroll', updateScrollDirection) // add event listener
return () => {
window.removeEventListener('scroll', updateScrollDirection) // clean up
}
}, [scrollDirection])
return scrollDirection
}
type ReaderHeaderProps = {
alwaysDisplayToolbar: boolean
hideDisplaySettings: boolean
@ -16,6 +42,8 @@ type ReaderHeaderProps = {
}
export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
const scrollDirection = useScrollDirection()
return (
<>
<VStack
@ -26,6 +54,7 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
left: '0',
zIndex: 1,
pt: '0px',
pb: '15px',
position: 'fixed',
width: '100%',
height: DEFAULT_HEADER_HEIGHT,
@ -37,8 +66,12 @@ export function ReaderHeader(props: ReaderHeaderProps): JSX.Element {
pointerEvents: 'unset',
},
'@mdDown': {
top: scrollDirection === 'down' ? '-90px' : '0',
bg: '$readerBg',
pointerEvents: 'unset',
transitionProperty: 'top',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '200ms',
},
'@media print': {
display: 'none',

View file

@ -181,11 +181,18 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
thLibrarySearchbox: '#FCFCFC',
thLibraryMenuPrimary: '#3D3D3D',
thLibraryMenuSecondary: '#3D3D3D',
thLibraryMenuUnselected: '#898989',
thLibraryMenuUnselected: '#3D3D3D',
thLibrarySelectionColor: '#FFEA9F',
thLibraryNavigationMenuFooter: '#EFEADE',
thLibraryMenuFooterHover: '#FFFFFF',
thFormInput: '#EBEBEB',
thHomeIcon: '#2A2A2A',
thLabelChipForeground: '#2A2A2A', // : '#2A2A2A'
thLabelChipBackground: '#F5F5F5',
thLabelChipSelectedBorder: 'black',
thLabelChipUnselectedBorder: '#F5F5F5',
thLabelOutlineChipBorder: '#D9D9D9',
thHeaderIconRing: '#D9D9D9',
thHeaderIconInner: '#898989',
@ -305,12 +312,19 @@ const darkThemeSpec = {
thLibrarySearchbox: '#3D3D3D',
thLibraryMenuPrimary: '#EBEBEB',
thLibraryMenuSecondary: '#EBEBEB',
thLibraryMenuUnselected: '#898989',
thLibrarySelectionColor: '#3D3D3D',
thLibraryMenuUnselected: 'white',
thLibrarySelectionColor: '#6A6968',
thLibraryNavigationMenuFooter: '#3D3D3D',
thLibraryMenuFooterHover: '#6A6968',
searchActiveOutline: '#866D15',
thFormInput: '#3D3D3D',
thHomeIcon: '#FFFFFF',
thLabelChipForeground: '#EBEBEB',
thLabelChipBackground: '#2A2A2A',
thLabelChipSelectedBorder: '#FFEA9F',
thLabelChipUnselectedBorder: '#2A2A2A',
thLabelOutlineChipBorder: '#6A696850',
thHeaderIconRing: '#3D3D3D',
thHeaderIconInner: '#D9D9D9',

View file

@ -99,6 +99,7 @@ export async function createHighlight(
patch,
id,
annotations.length > 0,
false,
input.color
)

View file

@ -4,7 +4,9 @@ import type { Highlight } from '../networking/fragments/highlightFragment'
import { interpolationSearch } from './interpolationSearch'
import {
highlightIdAttribute,
highlightLabelIdAttribute,
highlightNoteIdAttribute,
labelsImage,
noteImage,
} from './highlightHelpers'
@ -80,10 +82,18 @@ function nodeAttributesFromHighlight(
const patch = highlight.patch
const id = highlight.id
const withNote = !!highlight.annotation
const withLabels = (highlight.labels?.length ?? 0) > 0
const tooltip = undefined
const customColor = highlight.color
return makeHighlightNodeAttributes(patch, id, withNote, customColor, tooltip)
return makeHighlightNodeAttributes(
patch,
id,
withNote,
withLabels,
customColor,
tooltip
)
}
/**
@ -100,6 +110,7 @@ export function makeHighlightNodeAttributes(
patch: string,
id: string,
withNote: boolean,
withLabels: boolean,
customColor?: string,
tooltip?: string
): HighlightNodeAttributes {
@ -183,12 +194,10 @@ export function makeHighlightNodeAttributes(
startingTextNodeIndex++
}
if (withNote && lastElement) {
lastElement.classList.add('last_element')
const svg = noteImage(customColor)
svg.setAttribute(highlightNoteIdAttribute, id)
const ctr = document.createElement('div')
const ctr = document.createElement('span')
ctr.className = 'highlight_note_button'
ctr.appendChild(svg)
ctr.setAttribute(highlightNoteIdAttribute, id)
@ -197,6 +206,19 @@ export function makeHighlightNodeAttributes(
lastElement.appendChild(ctr)
}
if (withLabels && lastElement) {
const svg = labelsImage(customColor)
svg.setAttribute(highlightLabelIdAttribute, id)
const ctr = document.createElement('span')
ctr.className = 'highlight_label_button'
ctr.appendChild(svg)
ctr.setAttribute(highlightLabelIdAttribute, id)
ctr.setAttribute('width', '14px')
ctr.setAttribute('height', '14px')
lastElement.appendChild(ctr)
}
return {
prefix,
@ -209,10 +231,10 @@ export function makeHighlightNodeAttributes(
/**
* Given a text selection by user, annotate the article around the selection and
* produce a {@link https://github.com/google/diff-match-patch | diff patch}
*
* produce a {@link https://github.com/google/diff-match-patch | diff patch}
*
* The diff patch is used for identifying the selection/highlight location
*
*
* @param range text selection range
* @returns diff patch
*/
@ -304,11 +326,11 @@ const getArticleTextNodes = (
/**
* Return the offsets to the selection/highlight
*
*
* @param patch {@link generateDiffPatch|diff patch} identifying a selection/highlight location
* @returns
* @returns
* - highlightTextStart - The start of highlight, offset from the start of article by characters
* - highlightTextEnd - The end of highlight (non-inclusive), offset from the start of article by characters
* - highlightTextEnd - The end of highlight (non-inclusive), offset from the start of article by characters
* - matchingHighlightContent - the matched highlight
*/
const selectionOffsetsFromPatch = (

View file

@ -1,4 +1,4 @@
import { highlightColor, highlightColorVar } from '../themeUpdater'
import { highlightColorVar } from '../themeUpdater'
export type SelectionAttributes = {
selection: Selection
@ -14,6 +14,7 @@ export type SelectionAttributes = {
export const highlightIdAttribute = 'omnivore-highlight-id'
export const highlightNoteIdAttribute = 'omnivore-highlight-note-id'
export const highlightLabelIdAttribute = 'omnivore-highlight-label-id'
export function getHighlightElements(highlightId: string): Element[] {
return Array.from(
@ -46,3 +47,23 @@ export function noteImage(color: string | undefined): SVGSVGElement {
svg.appendChild(path)
return svg
}
export const labelsImage = (color: string | undefined): SVGSVGElement => {
const svgURI = 'http://www.w3.org/2000/svg'
const svg = document.createElementNS(svgURI, 'svg')
svg.setAttribute('viewBox', '0 0 14 14')
svg.setAttribute('width', '14')
svg.setAttribute('height', '14')
svg.setAttribute('fill', 'none')
const path = document.createElementNS(svgURI, 'path')
path.setAttribute(
'd',
'M1.75 3.5V6.517C1.75007 6.82639 1.87303 7.12309 2.09183 7.34183L6.58933 11.8393C6.85297 12.1029 7.21052 12.251 7.58333 12.251C7.95615 12.251 8.3137 12.1029 8.57733 11.8393L11.8393 8.57733C12.1029 8.3137 12.251 7.95615 12.251 7.58333C12.251 7.21052 12.1029 6.85297 11.8393 6.58933L7.34183 2.09183C7.12309 1.87303 6.82639 1.75007 6.517 1.75H3.5C3.03587 1.75 2.59075 1.93437 2.26256 2.26256C1.93437 2.59075 1.75 3.03587 1.75 3.5Z'
)
path.setAttribute('stroke', `rgba(${highlightColorVar(color)}, 0.8)`)
path.setAttribute('stroke-width', '1.8')
path.setAttribute('stroke-linejoin', 'round')
svg.appendChild(path)
return svg
}

View file

@ -16,6 +16,7 @@ export type Subscription = {
newsletterEmail?: string
url?: string
icon?: string
description?: string
status: SubscriptionStatus
@ -57,6 +58,7 @@ export function useGetSubscriptionsQuery(
type
newsletterEmail
url
icon
description
status
unsubscribeMailTo

View file

@ -0,0 +1,582 @@
import {
ReactNode,
useCallback,
useEffect,
useMemo,
useReducer,
useState,
} from 'react'
import { applyStoredTheme } from '../../lib/themeUpdater'
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
import { useGetSavedSearchQuery } from '../../lib/networking/queries/useGetSavedSearchQuery'
import { SettingsLayout } from '../../components/templates/SettingsLayout'
import { Toaster } from 'react-hot-toast'
import {
Box,
VStack,
HStack,
SpanBox,
Separator,
} from '../../components/elements/LayoutPrimitives'
import { LabelChip } from '../../components/elements/LabelChip'
import { StyledText } from '../../components/elements/StyledText'
import {
Subscription,
SubscriptionType,
useGetSubscriptionsQuery,
} from '../../lib/networking/queries/useGetSubscriptionsQuery'
import { DragIcon } from '../../components/elements/icons/DragIcon'
import { CoverImage } from '../../components/elements/CoverImage'
import { Label } from '../../lib/networking/fragments/labelFragment'
import { usePersistedState } from '../../lib/hooks/usePersistedState'
import { CheckSquare, Square } from 'phosphor-react'
import { Button } from '../../components/elements/Button'
import { styled } from '@stitches/react'
import { SavedSearch } from '../../lib/networking/fragments/savedSearchFragment'
type ListAction = 'RESET' | 'ADD_ITEM' | 'REMOVE_ITEM'
const SHORTCUTS_KEY = 'library-shortcuts'
export default function Shortcuts(): JSX.Element {
applyStoredTheme()
const [navMenuStyle, setNavMenuStyle] = usePersistedState<
'legacy' | 'shortcuts'
>({
key: 'library-nav-menu-style',
initialValue: 'legacy',
})
const listReducer = (
state: { state: string; items: Shortcut[] },
action: {
type: ListAction
item?: Shortcut
}
) => {
switch (action.type) {
case 'RESET': {
const itemStr = window['localStorage'].getItem(SHORTCUTS_KEY)
if (itemStr) {
try {
const parsed = JSON.parse(itemStr)
if (Array.isArray(parsed)) {
return { state: 'CURRENT', items: parsed as Shortcut[] }
}
} catch (err) {
console.log('error: ', err)
}
}
return { state: 'CURRENT', items: [] }
}
case 'ADD_ITEM': {
const item = action.item
if (!item) {
return state
}
const existing = state.items.find(
(existing) => existing.type == item.type && existing.id == item.id
)
if (existing) {
return state
}
state.items.push(item)
return { state: 'CURRENT', items: [...state.items] }
}
case 'REMOVE_ITEM': {
const item = action.item
if (!item) {
return state
}
const updated = state.items.filter((existing) => existing.id != item.id)
return { state: 'CURRENT', items: [...updated] }
}
default:
throw new Error('unknown action')
}
}
const [shortcuts, dispatchList] = useReducer(listReducer, {
state: 'INITIAL',
items: [],
})
useEffect(() => {
try {
if (shortcuts.state == 'CURRENT') {
window['localStorage'].setItem(
SHORTCUTS_KEY,
JSON.stringify(shortcuts.items)
)
}
} catch (error) {
console.log('error": ', error)
}
}, [shortcuts])
useEffect(() => {
dispatchList({ type: 'RESET' })
}, [])
return (
<SettingsLayout>
<Toaster
containerStyle={{
top: '5rem',
}}
/>
<VStack
css={{ width: '100%', height: '100%' }}
distribution="start"
alignment="center"
>
<VStack
css={{
padding: '24px',
width: '100%',
height: '100%',
gap: '25px',
minWidth: '300px',
maxWidth: '880px',
}}
>
<Box css={{}}>
<StyledText style="fixedHeadline" css={{ my: '6px' }}>
Shortcuts
</StyledText>
<StyledText style="caption" css={{}}>
Use shortcuts to access your most important reads quickly
</StyledText>
</Box>
<HStack
css={{
px: '10px',
pt: '2px',
height: '30px',
gap: '5px',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color:
navMenuStyle !== 'shortcuts'
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
'&:hover': {
backgroundColor: '$thBackground4',
},
}}
alignment="center"
distribution="start"
onClick={(event) => {
setNavMenuStyle(
navMenuStyle == 'shortcuts' ? 'legacy' : 'shortcuts'
)
event.preventDefault()
}}
>
{navMenuStyle === 'shortcuts' ? (
<CheckSquare size={20} weight="duotone" />
) : (
<Square size={20} weight="duotone" />
)}
<StyledText style="modalTitle" css={{}}>
Enable shortcuts
</StyledText>
</HStack>
<Box
css={{
py: '$3',
display: 'grid',
width: '100%',
gridAutoRows: 'auto',
borderRadius: '6px',
gridGap: '40px',
gridTemplateColumns: 'repeat(2, 1fr)',
'@mdDown': {
gridTemplateColumns: 'repeat(1, 1fr)',
},
}}
>
<SelectedItems
shortcuts={shortcuts.items}
dispatchList={dispatchList}
/>
<AvailableItems
shortcuts={shortcuts.items}
dispatchList={dispatchList}
/>
</Box>
</VStack>
</VStack>
</SettingsLayout>
)
}
export const SectionSeparator = styled(Separator, {
height: '1px',
my: '30px',
backgroundColor: '$grayBorder',
})
type ListProps = {
shortcuts: Shortcut[]
dispatchList: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
}
const AvailableItems = (props: ListProps): JSX.Element => {
const { labels } = useGetLabelsQuery()
const { savedSearches } = useGetSavedSearchQuery()
const { subscriptions } = useGetSubscriptionsQuery()
const sortedLabels = useMemo(() => {
if (!labels) {
return []
}
return labels.sort((a, b) =>
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
)
}, [labels])
const sortedSubscriptions = useMemo(() => {
if (!subscriptions) {
return []
}
return subscriptions.sort((a, b) =>
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
)
}, [subscriptions])
const sortedsavedSearches = useMemo(() => {
if (!savedSearches) {
return []
}
return savedSearches.sort((a, b) =>
a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())
)
}, [savedSearches])
const searchSelected = useCallback(
(search: SavedSearch) => {
return !!props.shortcuts.find((shortcut) => shortcut.id == search.id)
},
[props]
)
const labelSelected = useCallback(
(label: Label) => {
return !!props.shortcuts.find((shortcut) => shortcut.id == label.id)
},
[props]
)
const subscriptionSelected = useCallback(
(subscription: Subscription) => {
return !!props.shortcuts.find(
(shortcut) => shortcut.id == subscription.id
)
},
[props]
)
return (
<VStack
css={{
width: '420px',
py: '30px',
pl: '28px', // becomes labels have some margin built in
pr: '30px',
gap: '10px',
bg: '$thLeftMenuBackground',
}}
>
<StyledText style="settingsSection">Saved Searches</StyledText>
{sortedsavedSearches?.map((search) => {
return (
<Button
style="plainIcon"
css={{
display: 'flex',
width: '100%',
p: '5px',
alignItems: 'center',
borderRadius: '5px',
'&:hover': {
bg: '$thLibrarySelectionColor',
color: '$thLibraryMenuSecondary',
},
}}
key={`search-${search.id}`}
onClick={(event) => {
const item: Shortcut = {
id: search.id,
name: search.name,
type: 'label',
filter: search.filter,
}
props.dispatchList({
item,
type: searchSelected(search) ? 'REMOVE_ITEM' : 'ADD_ITEM',
})
event.preventDefault()
}}
>
{' '}
<StyledText style="settingsItem">{search.name}</StyledText>
<SpanBox css={{ ml: 'auto' }}>
{searchSelected(search) ? (
<CheckSquare size={20} weight="duotone" />
) : (
<Square size={20} weight="duotone" />
)}
</SpanBox>
</Button>
)
})}
<SectionSeparator />
<StyledText style="settingsSection">Labels</StyledText>
{sortedLabels.map((label) => {
return (
<Button
style="plainIcon"
css={{
display: 'flex',
width: '100%',
p: '5px',
alignItems: 'center',
borderRadius: '5px',
'&:hover': {
bg: '$thLibrarySelectionColor',
color: '$thLibraryMenuSecondary',
},
}}
key={`label-${label.id}`}
onClick={(event) => {
const item: Shortcut = {
id: label.id,
type: 'label',
label: label,
name: label.name,
filter: `label:\"${label.name}\"`,
}
props.dispatchList({
item,
type: labelSelected(label) ? 'REMOVE_ITEM' : 'ADD_ITEM',
})
event.preventDefault()
}}
>
<LabelChip text={label.name} color={label.color} />
<SpanBox css={{ ml: 'auto' }}>
{labelSelected(label) ? (
<CheckSquare size={20} weight="duotone" />
) : (
<Square size={20} weight="duotone" />
)}
</SpanBox>
</Button>
)
})}
<SectionSeparator />
<StyledText style="settingsSection">Subscriptions</StyledText>
{sortedSubscriptions.map((subscription) => {
return (
<Button
style="plainIcon"
css={{
display: 'flex',
width: '100%',
p: '5px',
alignItems: 'center',
borderRadius: '5px',
'&:hover': {
bg: '$thLibrarySelectionColor',
color: '$thLibraryMenuSecondary',
},
}}
key={`subscription-${subscription.id}`}
onClick={(event) => {
const item: Shortcut = {
id: subscription.id,
name: subscription.name,
icon: subscription.icon,
type:
subscription.type == SubscriptionType.NEWSLETTER
? 'newsletter'
: 'feed',
filter:
subscription.type == SubscriptionType.NEWSLETTER
? `subscription:\"${subscription.name}\"`
: `rss:\"${subscription.url}\"`,
}
props.dispatchList({
item,
type: subscriptionSelected(subscription)
? 'REMOVE_ITEM'
: 'ADD_ITEM',
})
event.preventDefault()
}}
>
<StyledText style="settingsItem">{subscription.name}</StyledText>
<SpanBox css={{ ml: 'auto' }}>
{subscriptionSelected(subscription) ? (
<CheckSquare size={20} weight="duotone" />
) : (
<Square size={20} weight="duotone" />
)}
</SpanBox>
</Button>
)
})}
</VStack>
)
}
type AvailableItemButtonProps = {
shortcut: Shortcut
isSelected: boolean
listAction: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
}
const AvailableItemButton = (props: AvailableItemButtonProps): JSX.Element => {
const shortcutId = `checkbox-search-${props.shortcut.id}`
return (
<CheckboxButton
itemKey={shortcutId}
title={props.shortcut.filter}
isSelected={props.isSelected}
item={props.shortcut}
listAction={props.listAction}
>
<SpanBox css={{}}>{props.shortcut.name}</SpanBox>
</CheckboxButton>
)
}
export type Shortcut = {
type: 'search' | 'label' | 'newsletter' | 'feed'
id: string
name: string
filter: string
icon?: string
label?: Label
}
const SelectedItems = (props: ListProps): JSX.Element => {
return (
<VStack
alignment="start"
distribution="start"
css={{
width: '420px',
py: '30px',
pl: '28px', // becomes labels have some margin built in
pr: '30px',
gap: '15px',
bg: '$thLeftMenuBackground',
}}
>
<StyledText style="settingsSection">Your shortcuts</StyledText>
{props.shortcuts.map((shortcut) => {
return (
<HStack
alignment="center"
distribution="start"
css={{ width: '100%', gap: '10px' }}
key={`search-${shortcut.id}`}
>
<CoverImage src={shortcut.icon} width={20} height={20} />
<StyledText style="settingsItem">{shortcut.name}</StyledText>
<HStack css={{ ml: 'auto' }}>
<DragIcon />
</HStack>
</HStack>
)
})}
</VStack>
)
}
type CheckboxButtonProps = {
itemKey: string
title: string
isSelected: boolean
item: Shortcut
listAction: (arg: { type: ListAction; item?: Shortcut | undefined }) => void
children: ReactNode
}
function CheckboxButton(props: CheckboxButtonProps): JSX.Element {
const handleChange = useCallback(
(selected: boolean) => {
if (!selected) {
props.listAction({
type: 'REMOVE_ITEM',
item: props.item,
})
} else {
props.listAction({
type: 'ADD_ITEM',
item: props.item,
})
}
},
[props]
)
return (
<HStack
key={props.itemKey}
title={props.title}
css={{
px: '10px',
pt: '2px',
height: '30px',
gap: '5px',
fontSize: '14px',
fontWeight: 'regular',
fontFamily: '$display',
color: props.isSelected
? '$thLibraryMenuSecondary'
: '$thLibraryMenuUnselected',
verticalAlign: 'middle',
borderRadius: '3px',
cursor: 'pointer',
m: '0px',
'&:hover': {
backgroundColor: '$thBackground4',
},
}}
alignment="center"
distribution="start"
onClick={(event) => {
handleChange(!props.isSelected)
event.preventDefault()
}}
>
{props.isSelected ? (
<CheckSquare size={20} weight="duotone" />
) : (
<Square size={20} weight="duotone" />
)}
{props.children}
</HStack>
)
}

View file

@ -44,23 +44,25 @@
font-family: var(--text-font-family);
}
.article-inner-css .highlight_with_note .highlight_note_button {
.article-inner-css .highlight_note_button {
display: unset !important;
margin: 0px !important;
max-width: unset !important;
height: unset !important;
padding: 0px 8px;
cursor: pointer;
margin-left: 5px !important;
}
.article-inner-css .highlight_label_button {
display: unset !important;
margin: 0px !important;
max-width: unset !important;
height: unset !important;
padding: 0px 8px;
cursor: pointer;
margin-left: 4px !important;
}
/*
on smaller screens we display the note icon
@media (max-width: 768px) {
.highlight_with_note.last_element:after {
content: url(/static/icons/highlight-note-icon.svg);
padding: 0 3px;
cursor: pointer;
}
} */
.article-inner-css h1,
.article-inner-css h2,