refactor: change highlight button to FAB style

- Replace floating popup button with persistent FAB in bottom-right corner
- Button always visible but disabled when no text is selected
- Uses user's highlight color from settings
- Visual feedback: scales up and becomes opaque when text is selected
- Follows Google apps design pattern for floating action buttons
This commit is contained in:
Gigi
2025-10-05 23:09:36 +01:00
parent f3a8cf1c23
commit cc68e67726
3 changed files with 37 additions and 46 deletions

2
dist/index.html vendored
View File

@@ -5,7 +5,7 @@
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Boris - Nostr Bookmarks</title>
<script type="module" crossorigin src="/assets/index-DnVl8eXy.js"></script>
<script type="module" crossorigin src="/assets/index-Bty_eGh6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bj-Uhit8.css">
</head>
<body>

View File

@@ -182,14 +182,14 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const handleMouseUp = useCallback(() => {
// Only allow highlight creation if user is logged in
if (!activeAccount || !relayPool) {
highlightButtonRef.current?.hide()
highlightButtonRef.current?.clearSelection()
return
}
setTimeout(() => {
const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) {
highlightButtonRef.current?.hide()
highlightButtonRef.current?.clearSelection()
return
}
@@ -197,9 +197,9 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const text = selection.toString().trim()
if (text.length > 0 && contentRef.current?.contains(range.commonAncestorContainer)) {
highlightButtonRef.current?.updateSelection(text, range.cloneRange())
highlightButtonRef.current?.updateSelection(text)
} else {
highlightButtonRef.current?.hide()
highlightButtonRef.current?.clearSelection()
}
}, 10)
}, [activeAccount, relayPool])
@@ -220,7 +220,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
)
onShowToast?.('Highlight created successfully!', 'success')
highlightButtonRef.current?.hide()
highlightButtonRef.current?.clearSelection()
window.getSelection()?.removeAllRanges()
// Trigger refresh of highlights
@@ -306,7 +306,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
{activeAccount && relayPool && (
<HighlightButton
ref={highlightButtonRef}
onHighlight={handleCreateHighlight}
onHighlight={handleCreateHighlight}
highlightColor={highlightColor}
/>
)}
</div>

View File

@@ -1,20 +1,21 @@
import React, { useCallback, useImperativeHandle, useRef } from 'react'
import React, { useCallback, useImperativeHandle, useRef, useState } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHighlighter } from '@fortawesome/free-solid-svg-icons'
interface HighlightButtonProps {
onHighlight: (text: string) => void
highlightColor?: string
}
export interface HighlightButtonRef {
updateSelection: (text: string, range: Range) => void
hide: () => void
updateSelection: (text: string) => void
clearSelection: () => void
}
export const HighlightButton = React.forwardRef<HighlightButtonRef, HighlightButtonProps>(
({ onHighlight }, ref) => {
({ onHighlight, highlightColor = '#ffff00' }, ref) => {
const currentSelectionRef = useRef<string>('')
const buttonRef = useRef<HTMLButtonElement>(null)
const [hasSelection, setHasSelection] = useState(false)
const handleClick = useCallback(
(e: React.MouseEvent) => {
@@ -27,59 +28,48 @@ export const HighlightButton = React.forwardRef<HighlightButtonRef, HighlightBut
[onHighlight]
)
const handleMouseDown = useCallback((e: React.MouseEvent) => {
// Prevent the button from taking focus away from the text selection
e.preventDefault()
}, [])
// Expose methods to update selection and hide button
// Expose methods to update selection
useImperativeHandle(ref, () => ({
updateSelection: (text: string, range: Range) => {
updateSelection: (text: string) => {
currentSelectionRef.current = text
if (buttonRef.current) {
const rect = range.getBoundingClientRect()
buttonRef.current.style.display = 'flex'
// Use fixed positioning relative to viewport, so it follows the scroll
buttonRef.current.style.top = `${rect.bottom + 8}px`
buttonRef.current.style.left = `${rect.left + rect.width / 2 - 20}px`
}
setHasSelection(!!text)
},
hide: () => {
clearSelection: () => {
currentSelectionRef.current = ''
if (buttonRef.current) {
buttonRef.current.style.display = 'none'
}
setHasSelection(false)
}
}))
return (
<button
ref={buttonRef}
className="highlight-create-button"
className="highlight-fab"
style={{
display: 'none',
position: 'fixed',
bottom: '32px',
right: '32px',
zIndex: 1000,
width: '40px',
height: '40px',
width: '56px',
height: '56px',
borderRadius: '50%',
backgroundColor: 'var(--color-primary, #0066cc)',
color: 'white',
border: '2px solid white',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.2)',
cursor: 'pointer',
backgroundColor: highlightColor,
color: '#000',
border: 'none',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
cursor: hasSelection ? 'pointer' : 'not-allowed',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
transition: 'all 0.3s ease',
opacity: hasSelection ? 1 : 0.4,
transform: hasSelection ? 'scale(1)' : 'scale(0.9)',
userSelect: 'none'
}}
onClick={handleClick}
onMouseDown={handleMouseDown}
tabIndex={-1}
aria-label="Create highlight"
title="Create highlight"
disabled={!hasSelection}
aria-label="Create highlight from selection"
title={hasSelection ? 'Create highlight' : 'Select text to highlight'}
>
<FontAwesomeIcon icon={faHighlighter} />
<FontAwesomeIcon icon={faHighlighter} size="lg" />
</button>
)
}