mirror of
https://github.com/dergigi/boris.git
synced 2026-01-26 18:24:22 +01:00
feat: add highlight style setting (marker & underline)
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface UserSettings {
|
||||
highlightsCollapsed?: boolean
|
||||
readingFont?: string
|
||||
fontSize?: number
|
||||
highlightStyle?: 'marker' | 'underline'
|
||||
}
|
||||
|
||||
export async function loadSettings(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user