Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 32 additions & 3 deletions src/components/bookmarks/BookmarkItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const BookmarkItem = forwardRef(function BookmarkItem(

const faviconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=32`

const sortedTags = Array.isArray(tags) ? [...tags].sort((a, b) => a.localeCompare(b)) : []

// Long-press detection for mobile context menu
const longPressTimer = useRef(null)
const isLongPress = useRef(false)
Expand Down Expand Up @@ -103,10 +105,14 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
: ''
}`}
>
{/* Checkbox */}
{/* Checkbox — on mobile only rendered in selection mode so it doesn't
reserve space or steal taps; on md+ it appears on hover */}
<button
onClick={handleCheckboxClick}
className={`flex-shrink-0 w-4 h-4 rounded border transition-all duration-150 flex items-center justify-center ${
aria-label={isChecked ? 'Deselect bookmark' : 'Select bookmark'}
className={`flex-shrink-0 w-4 h-4 rounded border transition-all duration-150 items-center justify-center ${
selectionMode || isChecked ? 'flex' : 'hidden md:flex'
} ${
isChecked
? 'bg-primary border-primary'
: selectionMode
Expand Down Expand Up @@ -146,7 +152,7 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
<span className="text-xs text-muted-foreground truncate flex-shrink-0 font-normal hidden md:inline">{domain}</span>
{tags && tags.length > 0 && (
<div className="items-center gap-1.5 flex-shrink-0 hidden md:flex">
{[...tags].sort((a, b) => a.localeCompare(b)).map((tag) => (
{sortedTags.map((tag) => (
<span
key={tag}
onClick={(e) => {
Expand All @@ -163,6 +169,29 @@ export const BookmarkItem = forwardRef(function BookmarkItem(
</div>
)}
</div>

{/* Second line on mobile: domain + tags (inline on md+) */}
{(domain || (tags && tags.length > 0)) && (
<div className="flex items-center gap-1.5 mt-1 overflow-hidden md:hidden">
{domain && (
<span className="text-xs text-muted-foreground truncate flex-shrink-0 max-w-[45%]">{domain}</span>
)}
{tags && tags.length > 0 && sortedTags.map((tag) => (
<span
key={tag}
onClick={(e) => {
e.stopPropagation()
if (!selectionMode) {
onTagClick && onTagClick(tag)
}
}}
className="text-[10px] leading-none px-2 py-1 rounded-full bg-secondary text-secondary-foreground flex-shrink-0 font-medium"
>
{tag}
</span>
))}
</div>
)}
</div>
</div>
)
Expand Down
50 changes: 37 additions & 13 deletions src/components/bookmarks/FilterBar.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { X, ChevronDown, Menu, Search, Plus, ListChecks } from '../ui/Icons'
import { X, ChevronDown, Menu, Search, Plus, ListChecks, ArrowUpDown } from '../ui/Icons'

const SORT_OPTIONS = [
{ value: 'recent', label: 'Recent' },
{ value: 'oldest', label: 'Oldest' },
{ value: 'title', label: 'A-Z' },
{ value: 'updated', label: 'Updated' },
]

export function FilterBar({
searchQuery,
Expand All @@ -23,11 +30,11 @@ export function FilterBar({
}

return (
<div className="sticky top-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-4 py-3">
<div className="flex items-center gap-3">
<div className="sticky top-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 px-3 sm:px-4 py-3">
<div className="flex items-center gap-1.5 sm:gap-3">
<button
onClick={onToggleSidebar}
className="lg:hidden p-2 -ml-2 hover:bg-accent rounded-md transition-colors"
className="lg:hidden h-9 w-9 -ml-1 flex-shrink-0 inline-flex items-center justify-center hover:bg-accent rounded-md transition-colors"
aria-label="Open sidebar"
>
<Menu className="w-5 h-5" strokeWidth={1.5} />
Expand All @@ -36,14 +43,14 @@ export function FilterBar({
{/* Spacer to align search with bookmark titles (accounts for checkbox + gap) */}
<div className="hidden lg:block w-7 flex-shrink-0" />

<div className="flex-1 relative">
<div className="flex-1 min-w-0 relative">
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none">
<Search className="w-4 h-4" strokeWidth={1.5} />
</div>
<input
ref={searchInputRef}
type="text"
placeholder="Search bookmarks..."
placeholder="Search..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
onKeyDown={handleKeyDown}
Expand All @@ -60,23 +67,40 @@ export function FilterBar({
)}
</div>

<div className="relative">
{/* Sort: icon-only trigger on mobile (invisible native select on top), text select on sm+ */}
<div className="relative sm:hidden h-9 w-9 flex-shrink-0">
<div className="h-full w-full inline-flex items-center justify-center rounded-md text-muted-foreground">
<ArrowUpDown className="w-4 h-4" strokeWidth={1.5} />
</div>
<select
value={sortBy}
onChange={(e) => onSortChange(e.target.value)}
aria-label="Sort bookmarks"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</div>

<div className="relative hidden sm:block flex-shrink-0">
<select
value={sortBy}
onChange={(e) => onSortChange(e.target.value)}
aria-label="Sort bookmarks"
className="appearance-none h-10 pl-3 pr-8 bg-transparent text-sm font-medium text-muted-foreground cursor-pointer rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background transition-colors"
>
<option value="recent">Recent</option>
<option value="oldest">Oldest</option>
<option value="title">A-Z</option>
<option value="updated">Updated</option>
{SORT_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
<ChevronDown className="w-3 h-3 absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none text-muted-foreground" strokeWidth={1.5} />
</div>

<button
onClick={onToggleSelectionMode}
className={`h-9 w-9 rounded-md inline-flex items-center justify-center transition-colors ${
className={`h-9 w-9 flex-shrink-0 rounded-md inline-flex items-center justify-center transition-colors ${
selectionMode
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
Expand All @@ -92,7 +116,7 @@ export function FilterBar({

<button
onClick={onAddNew}
className="h-9 px-4 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm font-medium text-sm inline-flex items-center gap-1.5 transition-colors"
className="h-9 px-3 sm:px-4 flex-shrink-0 rounded-md bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm font-medium text-sm inline-flex items-center gap-1.5 transition-colors"
aria-label="Add bookmark"
>
<Plus className="w-4 h-4" strokeWidth={2} />
Expand Down
31 changes: 17 additions & 14 deletions src/components/bookmarks/SelectionActionBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,29 @@ export function SelectionActionBar({ selectedCount, onTag, onReadLater, onDelete
if (selectedCount === 0) return null

return (
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-50 animate-in fade-in slide-in-from-bottom-4 duration-200">
<div className="flex items-center gap-2 px-4 py-2.5 bg-popover border border-border rounded-lg shadow-lg">
<span className="text-sm font-medium text-foreground pr-2">
{selectedCount} selected
<div className="fixed bottom-0 inset-x-0 z-50 pb-[env(safe-area-inset-bottom)] sm:pb-0 sm:bottom-6 sm:inset-x-auto sm:left-1/2 sm:-translate-x-1/2 animate-in fade-in slide-in-from-bottom-4 duration-200">
<div className="flex items-center justify-between sm:justify-start gap-1 sm:gap-2 px-3 sm:px-4 py-2.5 bg-popover border-t sm:border border-border sm:rounded-lg shadow-lg whitespace-nowrap">
<span className="text-sm font-medium text-foreground pr-1 sm:pr-2 flex-shrink-0">
<span className="hidden sm:inline">{selectedCount} selected</span>
<span className="sm:hidden">{selectedCount}</span>
</span>

<div className="w-px h-5 bg-border" />
<div className="w-px h-5 bg-border flex-shrink-0" />

{confirmingDelete ? (
<>
<span className="text-sm text-destructive font-medium">
<span className="text-sm text-destructive font-medium truncate">
Delete {selectedCount} bookmark{selectedCount > 1 ? 's' : ''}?
</span>
<button
onClick={() => { setConfirmingDelete(false); onDelete() }}
className="px-3 py-1.5 text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-md transition-colors"
className="px-3 py-2 text-sm font-medium bg-destructive text-destructive-foreground hover:bg-destructive/90 rounded-md transition-colors flex-shrink-0"
>
Confirm
</button>
<button
onClick={() => setConfirmingDelete(false)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors"
className="flex items-center gap-1.5 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors flex-shrink-0"
>
Cancel
</button>
Expand All @@ -37,31 +38,33 @@ export function SelectionActionBar({ selectedCount, onTag, onReadLater, onDelete
<>
<button
onClick={onTag}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground hover:bg-accent rounded-md transition-colors"
className="flex items-center gap-1.5 px-2.5 sm:px-3 py-2 text-sm font-medium text-foreground hover:bg-accent rounded-md transition-colors flex-shrink-0"
>
<Hash className="w-4 h-4" strokeWidth={1.5} />
Tag
</button>
<button
onClick={onReadLater}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-foreground hover:bg-accent rounded-md transition-colors"
className="flex items-center gap-1.5 px-2.5 sm:px-3 py-2 text-sm font-medium text-foreground hover:bg-accent rounded-md transition-colors flex-shrink-0"
>
<BookmarkPlus className="w-4 h-4" strokeWidth={1.5} />
Read Later
<span className="hidden min-[400px]:inline">Read Later</span>
<span className="min-[400px]:hidden">Later</span>
</button>
<button
onClick={() => setConfirmingDelete(true)}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-md transition-colors"
className="flex items-center gap-1.5 px-2.5 sm:px-3 py-2 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-md transition-colors flex-shrink-0"
>
<Trash className="w-4 h-4" strokeWidth={1.5} />
Delete {selectedCount}
</button>
<button
onClick={onCancel}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors"
aria-label="Cancel selection"
className="flex items-center gap-1.5 px-2.5 sm:px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-md transition-colors flex-shrink-0"
>
<X className="w-4 h-4" strokeWidth={1.5} />
Cancel
<span className="hidden sm:inline">Cancel</span>
</button>
</>
)}
Expand Down
1 change: 1 addition & 0 deletions src/components/ui/Icons.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export {
Square,
CheckSquare,
ListChecks,
ArrowUpDown,
Upload,
Cloud,
CloudOff,
Expand Down
Loading