feat: add highlight style setting (marker & underline)

This commit is contained in:
Gigi
2025-10-05 04:08:58 +01:00
parent e771b9778f
commit b59a295ad3
6 changed files with 86 additions and 19 deletions

View File

@@ -130,6 +130,7 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
selectedUrl={selectedUrl}
highlights={highlights}
showUnderlines={showUnderlines}
highlightStyle={settings.highlightStyle || 'marker'}
onHighlightClick={(id) => {
setSelectedHighlightId(id)
if (isHighlightsCollapsed) setIsHighlightsCollapsed(false)

View File

@@ -16,6 +16,7 @@ interface ContentPanelProps {
selectedUrl?: string
highlights?: Highlight[]
showUnderlines?: boolean
highlightStyle?: 'marker' | 'underline'
onHighlightClick?: (highlightId: string) => void
selectedHighlightId?: string
}
@@ -28,6 +29,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
selectedUrl,
highlights = [],
showUnderlines = true,
highlightStyle = 'marker',
onHighlightClick,
selectedHighlightId
}) => {
@@ -38,7 +40,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
useEffect(() => {
if (!selectedHighlightId || !contentRef.current) return
const markElement = contentRef.current.querySelector(`mark.content-highlight[data-highlight-id="${selectedHighlightId}"]`)
const markElement = contentRef.current.querySelector(`mark[data-highlight-id="${selectedHighlightId}"]`)
if (markElement) {
markElement.scrollIntoView({ behavior: 'smooth', block: 'center' })
@@ -86,18 +88,18 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
if (!contentRef.current || !originalHtmlRef.current) return
// Always apply highlights to the ORIGINAL HTML, not already-highlighted content
const highlightedHTML = applyHighlightsToHTML(originalHtmlRef.current, relevantHighlights)
const highlightedHTML = applyHighlightsToHTML(originalHtmlRef.current, relevantHighlights, highlightStyle)
contentRef.current.innerHTML = highlightedHTML
})
return () => cancelAnimationFrame(rafId)
}, [relevantHighlights, html, markdown, showUnderlines])
}, [relevantHighlights, html, markdown, showUnderlines, highlightStyle])
// Attach click handlers separately (only when handler changes)
useEffect(() => {
if (!onHighlightClick || !contentRef.current) return
const marks = contentRef.current.querySelectorAll('mark.content-highlight')
const marks = contentRef.current.querySelectorAll('mark[data-highlight-id]')
const handlers = new Map<Element, () => void>()
marks.forEach(mark => {

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react'
import { faTimes, faList, faThLarge, faImage } from '@fortawesome/free-solid-svg-icons'
import { faTimes, faList, faThLarge, faImage, faUnderline, faHighlighter } from '@fortawesome/free-solid-svg-icons'
import { UserSettings } from '../services/settingsService'
import IconButton from './IconButton'
import { loadFont, getFontFamily } from '../utils/fontLoader'
@@ -111,6 +111,26 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
</label>
</div>
<div className="setting-group setting-inline">
<label>Highlight Style</label>
<div className="setting-buttons">
<IconButton
icon={faHighlighter}
onClick={() => setLocalSettings({ ...localSettings, highlightStyle: 'marker' })}
title="Text marker style"
ariaLabel="Text marker style"
variant={(localSettings.highlightStyle || 'marker') === 'marker' ? 'primary' : 'ghost'}
/>
<IconButton
icon={faUnderline}
onClick={() => setLocalSettings({ ...localSettings, highlightStyle: 'underline' })}
title="Underline style"
ariaLabel="Underline style"
variant={localSettings.highlightStyle === 'underline' ? 'primary' : 'ghost'}
/>
</div>
</div>
<div className="setting-preview">
<div className="preview-label">Preview</div>
<div
@@ -121,7 +141,7 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
}}
>
<h3>The Quick Brown Fox</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <span className={localSettings.showUnderlines !== false ? "content-highlight" : ""}>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span> Ut enim ad minim veniam.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. <span className={localSettings.showUnderlines !== false ? `content-highlight-${localSettings.highlightStyle || 'marker'}` : ""}>Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</span> Ut enim ad minim veniam.</p>
<p>Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
</div>
</div>

View File

@@ -1402,7 +1402,8 @@ body {
}
/* Inline content highlights - fluorescent marker style */
.content-highlight {
.content-highlight,
.content-highlight-marker {
background: rgba(255, 255, 0, 0.35);
padding: 0.125rem 0.25rem;
cursor: pointer;
@@ -1412,12 +1413,33 @@ body {
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
}
.content-highlight:hover {
.content-highlight:hover,
.content-highlight-marker:hover {
background: rgba(255, 255, 0, 0.5);
box-shadow: 0 0 12px rgba(255, 255, 0, 0.3);
}
.content-highlight.highlight-pulse {
/* Underline style for highlights */
.content-highlight-underline {
background: transparent;
padding: 0;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
text-decoration: underline;
text-decoration-color: rgba(255, 200, 0, 0.8);
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
.content-highlight-underline:hover {
text-decoration-color: rgba(255, 200, 0, 1);
text-decoration-thickness: 3px;
}
.content-highlight.highlight-pulse,
.content-highlight-marker.highlight-pulse,
.content-highlight-underline.highlight-pulse {
animation: highlight-pulse-animation 1.5s ease-in-out;
}
@@ -1441,23 +1463,43 @@ body {
}
.reader-html .content-highlight,
.reader-markdown .content-highlight {
.reader-markdown .content-highlight,
.reader-html .content-highlight-marker,
.reader-markdown .content-highlight-marker,
.reader-html .content-highlight-underline,
.reader-markdown .content-highlight-underline {
color: inherit;
}
.reader-html .content-highlight,
.reader-markdown .content-highlight,
.reader-html .content-highlight-marker,
.reader-markdown .content-highlight-marker {
text-decoration: none;
}
/* Ensure highlights work in both light and dark mode */
@media (prefers-color-scheme: light) {
.content-highlight {
.content-highlight,
.content-highlight-marker {
background: rgba(255, 255, 0, 0.4);
box-shadow: 0 0 6px rgba(255, 255, 0, 0.15);
}
.content-highlight:hover {
.content-highlight:hover,
.content-highlight-marker:hover {
background: rgba(255, 255, 0, 0.55);
box-shadow: 0 0 10px rgba(255, 255, 0, 0.25);
}
.content-highlight-underline {
text-decoration-color: rgba(255, 180, 0, 0.9);
}
.content-highlight-underline:hover {
text-decoration-color: rgba(255, 180, 0, 1);
}
.highlight-indicator {
background: rgba(100, 108, 255, 0.15);
border-color: rgba(100, 108, 255, 0.4);

View File

@@ -16,6 +16,7 @@ export interface UserSettings {
highlightsCollapsed?: boolean
readingFont?: string
fontSize?: number
highlightStyle?: 'marker' | 'underline'
}
export async function loadSettings(

View File

@@ -99,9 +99,9 @@ export function applyHighlightsToText(
const normalizeWhitespace = (str: string) => str.replace(/\s+/g, ' ').trim()
// Helper to create a mark element for a highlight
function createMarkElement(highlight: Highlight, matchText: string): HTMLElement {
function createMarkElement(highlight: Highlight, matchText: string, highlightStyle: 'marker' | 'underline' = 'marker'): HTMLElement {
const mark = document.createElement('mark')
mark.className = 'content-highlight'
mark.className = `content-highlight-${highlightStyle}`
mark.setAttribute('data-highlight-id', highlight.id)
mark.setAttribute('title', `Highlighted ${new Date(highlight.created_at * 1000).toLocaleDateString()}`)
mark.textContent = matchText
@@ -127,7 +127,8 @@ function tryMarkInTextNodes(
textNodes: Text[],
searchText: string,
highlight: Highlight,
useNormalized: boolean
useNormalized: boolean,
highlightStyle: 'marker' | 'underline' = 'marker'
): boolean {
const normalizedSearch = normalizeWhitespace(searchText)
@@ -154,7 +155,7 @@ function tryMarkInTextNodes(
const before = text.substring(0, actualIndex)
const match = text.substring(actualIndex, actualIndex + searchText.length)
const after = text.substring(actualIndex + searchText.length)
const mark = createMarkElement(highlight, match)
const mark = createMarkElement(highlight, match, highlightStyle)
replaceTextWithMark(textNode, before, after, mark)
return true
@@ -166,7 +167,7 @@ function tryMarkInTextNodes(
/**
* Apply highlights to HTML content by injecting mark tags using DOM manipulation
*/
export function applyHighlightsToHTML(html: string, highlights: Highlight[]): string {
export function applyHighlightsToHTML(html: string, highlights: Highlight[], highlightStyle: 'marker' | 'underline' = 'marker'): string {
if (!html || highlights.length === 0) return html
const tempDiv = document.createElement('div')
@@ -183,8 +184,8 @@ export function applyHighlightsToHTML(html: string, highlights: Highlight[]): st
while ((node = walker.nextNode())) textNodes.push(node as Text)
// Try exact match first, then normalized match
tryMarkInTextNodes(textNodes, searchText, highlight, false) ||
tryMarkInTextNodes(textNodes, searchText, highlight, true)
tryMarkInTextNodes(textNodes, searchText, highlight, false, highlightStyle) ||
tryMarkInTextNodes(textNodes, searchText, highlight, true, highlightStyle)
}
return tempDiv.innerHTML