diff --git a/src/components/Bookmarks.tsx b/src/components/Bookmarks.tsx index a5697ca9..eb9d7635 100644 --- a/src/components/Bookmarks.tsx +++ b/src/components/Bookmarks.tsx @@ -130,6 +130,7 @@ const Bookmarks: React.FC = ({ relayPool, onLogout }) => { selectedUrl={selectedUrl} highlights={highlights} showUnderlines={showUnderlines} + highlightStyle={settings.highlightStyle || 'marker'} onHighlightClick={(id) => { setSelectedHighlightId(id) if (isHighlightsCollapsed) setIsHighlightsCollapsed(false) diff --git a/src/components/ContentPanel.tsx b/src/components/ContentPanel.tsx index 871ae898..59a996d8 100644 --- a/src/components/ContentPanel.tsx +++ b/src/components/ContentPanel.tsx @@ -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 = ({ selectedUrl, highlights = [], showUnderlines = true, + highlightStyle = 'marker', onHighlightClick, selectedHighlightId }) => { @@ -38,7 +40,7 @@ const ContentPanel: React.FC = ({ 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 = ({ 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 void>() marks.forEach(mark => { diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 5210ed80..24f5e6ce 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -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 = ({ settings, onSave, onClose }) => { +
+ +
+ setLocalSettings({ ...localSettings, highlightStyle: 'marker' })} + title="Text marker style" + ariaLabel="Text marker style" + variant={(localSettings.highlightStyle || 'marker') === 'marker' ? 'primary' : 'ghost'} + /> + setLocalSettings({ ...localSettings, highlightStyle: 'underline' })} + title="Underline style" + ariaLabel="Underline style" + variant={localSettings.highlightStyle === 'underline' ? 'primary' : 'ghost'} + /> +
+
+
Preview
= ({ settings, onSave, onClose }) => { }} >

The Quick Brown Fox

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam.

Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.

diff --git a/src/index.css b/src/index.css index aa777b10..35f49bba 100644 --- a/src/index.css +++ b/src/index.css @@ -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); diff --git a/src/services/settingsService.ts b/src/services/settingsService.ts index 3bf41ef7..d4bea1ee 100644 --- a/src/services/settingsService.ts +++ b/src/services/settingsService.ts @@ -16,6 +16,7 @@ export interface UserSettings { highlightsCollapsed?: boolean readingFont?: string fontSize?: number + highlightStyle?: 'marker' | 'underline' } export async function loadSettings( diff --git a/src/utils/highlightMatching.tsx b/src/utils/highlightMatching.tsx index 619a7aa8..31d70883 100644 --- a/src/utils/highlightMatching.tsx +++ b/src/utils/highlightMatching.tsx @@ -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