add a menu for bookmark list; remove btns

This commit is contained in:
2025-06-03 15:41:45 +00:00
parent 3fcd90af69
commit cd5147f3cf
3 changed files with 89 additions and 141 deletions

View File

@@ -2,8 +2,8 @@ import type { Bookmark } from "@markone/core"
import { Link } from "@tanstack/react-router"
import clsx from "clsx"
import { memo, useCallback, useRef } from "react"
import { Menu, MenuButton, MenuItems, MenuItem } from "@headlessui/react"
import { useBookmarkTags } from "~/bookmark/api"
import { Button } from "~/components/button"
import { List, type ListRef } from "~/components/list"
import { DialogKind, ActionBarContentKind, useBookmarkPageStore } from "./-store"
import { useMnemonics } from "~/hooks/use-mnemonics"
@@ -18,7 +18,6 @@ export enum BookmarkListItemAction {
interface BookmarkListProps {
bookmarks: Bookmark[]
selectedBookmarkId?: string
alwaysExpandItem: boolean
onSelectionChange?: (bookmark: Bookmark) => void
onItemAction: (bookmark: Bookmark, action: BookmarkListItemAction) => void
className?: string
@@ -38,93 +37,104 @@ const BookmarkTagList = memo(function BookmarkTagList({ bookmark }: { bookmark:
) : null
})
const BookmarkListItem = memo(function BookmarkListItem({
const BookmarkItemMenu = memo(function BookmarkItemMenu({
bookmark,
isSelected,
isExpanded,
onSelect,
onExpand,
onItemAction,
alwaysExpandItem,
}: {
bookmark: Bookmark
isSelected: boolean
isExpanded: boolean
onSelect: () => void
onExpand: () => void
onItemAction: (bookmark: Bookmark, action: BookmarkListItemAction) => void
alwaysExpandItem: boolean
}) {
const url = new URL(bookmark.url)
return (
<div
className={clsx("group flex flex-row justify-start py-2", {
"bg-teal-600 text-stone-100": isExpanded,
"text-teal-600": isSelected && !isExpanded,
})}
>
<button
<Menu as="div" className="relative">
<MenuButton
type="button"
disabled={alwaysExpandItem || !isSelected}
className={clsx("select-none flex items-start font-bold hover:bg-teal-600 hover:text-stone-100", {
invisible: !isSelected,
})}
onClick={onExpand}
className="select-none flex items-start font-bold hover:bg-teal-600 hover:text-stone-100 px-2 focus:outline-none data-open:bg-stone-900 data-open:dark:bg-stone-100 data-open:text-stone-100 data-open:dark:text-stone-900"
>
<span className="sr-only">Options for this bookmark</span>
<span>&nbsp;</span>
<span className={isExpanded ? "rotate-90" : ""}>&gt;</span>
<span>&nbsp;</span>
</button>
<div className="flex flex-col w-full">
<Link
to={`/bookmarks/${bookmark.id}`}
className={clsx("block w-full text-start font-bold", { underline: isSelected })}
>
{bookmark.title}
</Link>
<p className="opacity-80 text-sm">{url.host}</p>
{isExpanded ? (
<>
<BookmarkTagList bookmark={bookmark} />
<div className="flex flex-col space-y-1 md:flex-row md:space-y-0 md:space-x-2 items-end justify-between pt-2">
<div className="flex space-x-2">
<Button
variant="light"
className="text-sm"
onClick={() => onItemAction(bookmark, BookmarkListItemAction.CopyLink)}
>
<span>COPY LINK</span>
</Button>
<Button
variant="light"
className="text-sm"
onClick={() => onItemAction(bookmark, BookmarkListItemAction.Edit)}
>
<span className="underline">E</span>DIT
</Button>
<Button
variant="light"
className="text-sm"
onClick={() => onItemAction(bookmark, BookmarkListItemAction.Delete)}
>
<span className="underline">D</span>ELETE
</Button>
<span className="-ml-2">&nbsp;</span>
</div>
</div>
</>
) : null}
</div>
</div>
<span className="sr-only">More options for {bookmark.title}</span>
<span className="text-xl"></span>
</MenuButton>
<MenuItems
anchor="bottom end"
className="w-48 origin-top-right bg-stone-300 dark:bg-stone-800 border-2 border-stone-900 dark:border-stone-100 focus:outline-none"
>
<div className="py-2">
<MenuItem
as="button"
type="button"
className="block w-full pl-3 pr-4 py-1 text-left text-sm font-bold text-stone-900 dark:text-stone-100 data-[focus]:bg-stone-800 data-[focus]:dark:bg-stone-300 data-[focus]:text-stone-300 data-[focus]:dark:text-stone-800"
onClick={() => onItemAction(bookmark, BookmarkListItemAction.CopyLink)}
>
COPY LINK
</MenuItem>
<MenuItem
as="button"
type="button"
className="block w-full pl-3 pr-4 py-1 text-left text-sm font-bold text-stone-900 dark:text-stone-100 data-[focus]:bg-stone-800 data-[focus]:dark:bg-stone-300 data-[focus]:text-stone-300 data-[focus]:dark:text-stone-800"
onClick={() => onItemAction(bookmark, BookmarkListItemAction.Edit)}
>
<span className="underline">E</span>DIT
</MenuItem>
<MenuItem
as="button"
type="button"
className="block w-full pl-3 pr-4 py-1 text-left text-sm font-bold text-stone-900 dark:text-stone-100 data-[focus]:bg-stone-800 data-[focus]:dark:bg-stone-300 data-[focus]:text-stone-300 data-[focus]:dark:text-stone-800"
onClick={() => onItemAction(bookmark, BookmarkListItemAction.Delete)}
>
<span className="underline">D</span>ELETE
</MenuItem>
</div>
</MenuItems>
</Menu>
)
})
const BookmarkListItem = memo(
({
bookmark,
isSelected,
onItemAction,
}: {
bookmark: Bookmark
isSelected: boolean
onItemAction: (bookmark: Bookmark, action: BookmarkListItemAction) => void
}) => {
const url = new URL(bookmark.url)
return (
<div
className={clsx("group flex flex-row justify-start py-2", {
"text-teal-600": isSelected,
})}
>
<span
className={clsx("select-none flex items-start font-bold", {
invisible: !isSelected,
})}
>
<span>&nbsp;</span>
<span>&gt;</span>
<span>&nbsp;</span>
</span>
<div className="flex flex-col w-full">
<div className="flex justify-between items-start">
<Link
to={`/bookmarks/${bookmark.id}`}
className={clsx("block w-full text-start font-bold", { underline: isSelected })}
>
{bookmark.title}
</Link>
{isSelected ? <BookmarkItemMenu bookmark={bookmark} onItemAction={onItemAction} /> : null}
</div>
<p className="opacity-80 text-sm">{url.host}</p>
</div>
</div>
)
},
)
function BookmarkList({
bookmarks,
selectedBookmarkId,
alwaysExpandItem,
onSelectionChange,
onItemAction,
className,
@@ -165,20 +175,11 @@ function BookmarkList({
ref={listRef}
items={bookmarks}
selectedItemId={selectedBookmarkId}
alwaysExpandItem={alwaysExpandItem}
onSelectionChange={handleSelect}
className={className}
emptyMessage="No bookmarks found!"
renderItem={({ item: bookmark, isSelected, isExpanded, onSelect, onExpand }) => (
<BookmarkListItem
bookmark={bookmark}
isSelected={isSelected}
isExpanded={isExpanded}
onSelect={onSelect}
onExpand={onExpand}
onItemAction={onItemAction}
alwaysExpandItem={alwaysExpandItem}
/>
renderItem={({ item: bookmark, isSelected, onSelect }) => (
<BookmarkListItem bookmark={bookmark} isSelected={isSelected} onItemAction={onItemAction} />
)}
/>
)

View File

@@ -79,7 +79,6 @@ function BookmarkListContainer() {
return (
<BookmarkList
className={bookmarks.length > 0 ? "-mt-2" : ""}
alwaysExpandItem={false}
bookmarks={bookmarks}
onItemAction={handleBookmarkListItemAction}
/>

View File

@@ -16,12 +16,9 @@ interface ListState<T extends ListData> {
items: T[]
selectedIndex: number
selectedItemId: string
isItemExpanded: boolean
alwaysExpandItem: boolean
setItems: (items: T[]) => void
setSelectedIndex: (index: number) => void
setSelectedItemId: (id: string) => void
setIsItemExpanded: (expanded: boolean) => void
}
type ListStore<T extends ListData> = ReturnType<typeof createListStore<T>>
@@ -33,14 +30,11 @@ type MnemonicCallback<T> = (event: KeyboardEvent, item: T) => void
interface ListProps<T extends ListData> {
items: T[]
selectedItemId?: string
alwaysExpandItem?: boolean
onSelectionChange?: (item: T) => void
renderItem: (props: {
item: T
isSelected: boolean
isExpanded: boolean
onSelect: () => void
onExpand: () => void
}) => React.ReactNode
className?: string
emptyMessage?: string
@@ -51,7 +45,6 @@ interface ListProps<T extends ListData> {
function List<T extends ListData>({
items,
selectedItemId,
alwaysExpandItem = false,
onSelectionChange,
renderItem,
className,
@@ -59,7 +52,7 @@ function List<T extends ListData>({
mnemonics,
ref,
}: ListProps<T>) {
const storeRef = useRef<ListStore<T>>(createListStore({ items, selectedItemId, alwaysExpandItem }))
const storeRef = useRef<ListStore<T>>(createListStore({ items, selectedItemId }))
useImperativeHandle(
ref,
@@ -107,11 +100,9 @@ function List<T extends ListData>({
function createListStore<T extends ListData>({
items,
selectedItemId,
alwaysExpandItem = false,
}: {
items: T[]
selectedItemId?: string
alwaysExpandItem?: boolean
}) {
let _selectedItemId = selectedItemId
if (!_selectedItemId && items.length > 0) {
@@ -121,10 +112,8 @@ function createListStore<T extends ListData>({
return createStore<ListState<T>>()(
subscribeWithSelector((set) => ({
items,
alwaysExpandItem,
selectedIndex: selectedItemId ? items.findIndex((item) => item.id === selectedItemId) : 0,
selectedItemId: _selectedItemId ?? "",
isItemExpanded: alwaysExpandItem,
setItems(items: T[]) {
set({
@@ -141,10 +130,6 @@ function createListStore<T extends ListData>({
setSelectedItemId(id: string) {
set({ selectedItemId: id })
},
setIsItemExpanded(expanded: boolean) {
set({ isItemExpanded: expanded })
},
})),
)
}
@@ -164,27 +149,20 @@ function ListItem<T extends ListData>({
item,
index,
isSelected,
isExpanded,
onSelect,
onExpand,
renderItem,
}: {
item: T
index: number
isSelected: boolean
isExpanded: boolean
onSelect: (id: string) => void
onExpand: (expanded: boolean) => void
renderItem: (props: {
item: T
isSelected: boolean
isExpanded: boolean
onSelect: () => void
onExpand: () => void
}) => React.ReactNode
}) {
const setSelectedIndex = useListStore((state) => state.setSelectedIndex)
const isItemExpanded = useListStore((state) => state.isItemExpanded)
useEffect(() => {
if (isSelected) {
@@ -196,24 +174,16 @@ function ListItem<T extends ListData>({
onSelect(item.id)
}, [item.id, onSelect])
const handleExpand = useCallback(() => {
onExpand(!isExpanded)
}, [isExpanded, onExpand])
const handleMouseEnter = useCallback(() => {
if (!isItemExpanded) {
onSelect(item.id)
}
}, [isItemExpanded, item.id, onSelect])
onSelect(item.id)
}, [item.id, onSelect])
return (
<li onMouseEnter={handleMouseEnter}>
{renderItem({
item,
isSelected,
isExpanded,
onSelect: handleSelect,
onExpand: handleExpand,
})}
</li>
)
@@ -231,9 +201,7 @@ function _List<T extends ListData>({
renderItem: (props: {
item: T
isSelected: boolean
isExpanded: boolean
onSelect: () => void
onExpand: () => void
}) => React.ReactNode
emptyMessage: string
mnemonics?: Record<string, MnemonicCallback<T>>
@@ -241,9 +209,7 @@ function _List<T extends ListData>({
const store = useListStoreContext<T>()
const items = useListStore<T, T[]>((state) => state.items)
const selectedItemId = useListStore<T, string>((state) => state.selectedItemId)
const isItemExpanded = useListStore<T, boolean>((state) => state.isItemExpanded)
const setSelectedItemId = useListStore<T, (id: string) => void>((state) => state.setSelectedItemId)
const setIsItemExpanded = useListStore<T, (expanded: boolean) => void>((state) => state.setIsItemExpanded)
const shortcuts = useMemo(() => {
const baseShortcuts: Record<string, (event: KeyboardEvent) => void> = {
@@ -251,10 +217,6 @@ function _List<T extends ListData>({
ArrowDown: selectNextItem,
k: selectPrevItem,
ArrowUp: selectPrevItem,
l: expandItem,
ArrowRight: expandItem,
h: collapseItem,
ArrowLeft: collapseItem,
}
if (!mnemonics) {
@@ -291,17 +253,6 @@ function _List<T extends ListData>({
}
}
function expandItem() {
store.getState().setIsItemExpanded(true)
}
function collapseItem() {
const state = store.getState()
if (!state.alwaysExpandItem) {
store.getState().setIsItemExpanded(false)
}
}
return (
<ul className={clsx("flex flex-col", className)}>
{items.length === 0 ? (
@@ -309,7 +260,6 @@ function _List<T extends ListData>({
) : (
items.map((item, index) => {
const isSelected = item.id === selectedItemId
const isExpanded = isItemExpanded && isSelected
return (
<MemoizedListItem
@@ -317,9 +267,7 @@ function _List<T extends ListData>({
item={item}
index={index}
isSelected={isSelected}
isExpanded={isExpanded}
onSelect={setSelectedItemId}
onExpand={setIsItemExpanded}
renderItem={renderItem}
/>
)