From e6a7bb4c9854eef5455e40bd6e5cf155b1f2e73d Mon Sep 17 00:00:00 2001 From: Gigi Date: Sun, 12 Oct 2025 23:59:28 +0200 Subject: [PATCH] feat: add three-dot menu to highlight cards - Replace external link button with three-dot menu in highlight cards - Move 'Open source/Nostr' and 'Delete' actions into dropdown menu - Add click-outside functionality to close menu - Style menu for both dark and light themes --- src/components/HighlightItem.tsx | 95 +++++++++++++++++++++------- src/index.css | 103 +++++++++++++++++++++++++++---- 2 files changed, 166 insertions(+), 32 deletions(-) diff --git a/src/components/HighlightItem.tsx b/src/components/HighlightItem.tsx index 91cc7f03..135cae93 100644 --- a/src/components/HighlightItem.tsx +++ b/src/components/HighlightItem.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash } from '@fortawesome/free-solid-svg-icons' +import { faQuoteLeft, faExternalLinkAlt, faPlane, faSpinner, faServer, faTrash, faEllipsisV } from '@fortawesome/free-solid-svg-icons' import { Highlight } from '../types/highlights' import { useEventModel } from 'applesauce-react/hooks' import { Models, IEventStore } from 'applesauce-core' @@ -40,11 +40,13 @@ export const HighlightItem: React.FC = ({ onHighlightDelete }) => { const itemRef = useRef(null) + const menuRef = useRef(null) const [isSyncing, setIsSyncing] = useState(() => isEventSyncing(highlight.id)) const [showOfflineIndicator, setShowOfflineIndicator] = useState(() => highlight.isOfflineCreated && !isSyncing) const [isRebroadcasting, setIsRebroadcasting] = useState(false) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [isDeleting, setIsDeleting] = useState(false) + const [showMenu, setShowMenu] = useState(false) const activeAccount = Hooks.useActiveAccount() @@ -97,6 +99,22 @@ export const HighlightItem: React.FC = ({ } }, [isSelected]) + // Close menu when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setShowMenu(false) + } + } + + if (showMenu) { + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + } + }, [showMenu]) + const handleItemClick = () => { if (onHighlightClick) { onHighlightClick(highlight.id) @@ -295,6 +313,29 @@ export const HighlightItem: React.FC = ({ setShowDeleteConfirm(false) } + const handleMenuToggle = (e: React.MouseEvent) => { + e.stopPropagation() + setShowMenu(!showMenu) + } + + const handleOpenExternal = (e: React.MouseEvent) => { + e.stopPropagation() + if (sourceLink) { + if (highlight.urlReference && onSelectUrl) { + onSelectUrl(highlight.urlReference) + } else { + window.open(sourceLink, '_blank', 'noopener,noreferrer') + } + } + setShowMenu(false) + } + + const handleMenuDeleteClick = (e: React.MouseEvent) => { + e.stopPropagation() + setShowMenu(false) + setShowDeleteConfirm(true) + } + return ( <>
= ({
)} - {canDelete && ( -
- -
- )}
@@ -348,18 +380,39 @@ export const HighlightItem: React.FC = ({ {formatDateCompact(highlight.created_at)} - {sourceLink && ( - highlight.urlReference && onSelectUrl ? handleLinkClick(highlight.urlReference, e) : undefined} - className="highlight-source" - title={highlight.eventReference ? 'Open on Nostr' : 'Open source'} +
+ + + {showMenu && ( +
+ {sourceLink && ( + + )} + {canDelete && ( + + )} +
+ )} +
diff --git a/src/index.css b/src/index.css index e4813c3e..a5217673 100644 --- a/src/index.css +++ b/src/index.css @@ -1783,6 +1783,35 @@ body.mobile-sidebar-open { .highlight-text { color: #213547; } + + .highlight-menu-btn { + color: #666; + } + + .highlight-menu-btn:hover { + color: #646cff; + background: rgba(100, 108, 255, 0.08); + } + + .highlight-menu { + background: #fff; + border-color: #ddd; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + } + + .highlight-menu-item { + color: #213547; + } + + .highlight-menu-item:hover { + background: rgba(100, 108, 255, 0.08); + color: #000; + } + + .highlight-menu-item-danger:hover { + background: rgba(255, 68, 68, 0.08); + color: #cc0000; + } } /* Highlights Panel Styles */ @@ -2236,25 +2265,77 @@ body.mobile-sidebar-open { line-height: 1; } -.highlight-source { +.highlight-menu-wrapper { + position: relative; + margin-left: auto; + flex-shrink: 0; +} + +.highlight-menu-btn { + background: none; + border: none; + color: #888; + cursor: pointer; + padding: 0.25rem 0.5rem; + font-size: 0.875rem; display: flex; align-items: center; - gap: 0.375rem; - color: #646cff; - text-decoration: none; transition: color 0.2s ease; - flex-shrink: 0; - margin-left: auto; - line-height: 1; + border-radius: 4px; } -.highlight-source:hover { - color: #535bf2; - text-decoration: underline; +.highlight-menu-btn:hover { + color: #646cff; + background: rgba(100, 108, 255, 0.1); } -.highlight-source svg { +.highlight-menu { + position: absolute; + right: 0; + top: calc(100% + 4px); + background: #2a2a2a; + border: 1px solid #444; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + z-index: 1000; + min-width: 160px; + overflow: hidden; +} + +.highlight-menu-item { + width: 100%; + background: none; + border: none; + color: #ddd; + padding: 0.625rem 0.875rem; font-size: 0.875rem; + display: flex; + align-items: center; + gap: 0.625rem; + cursor: pointer; + transition: all 0.15s ease; + text-align: left; + white-space: nowrap; +} + +.highlight-menu-item:hover { + background: rgba(100, 108, 255, 0.15); + color: #fff; +} + +.highlight-menu-item:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.highlight-menu-item-danger:hover { + background: rgba(255, 68, 68, 0.15); + color: #ff4444; +} + +.highlight-menu-item svg { + font-size: 0.875rem; + flex-shrink: 0; } /* Inline content highlights - fluorescent marker style */