feat: add configurable highlight colors

- Add highlightColor setting with 6 preset colors (yellow, orange, pink, green, blue, purple)
- Implement color picker UI with square color swatches
- Use CSS variables to dynamically apply highlight colors
- Add hex to RGB conversion for color transparency support
- Update both marker and underline styles to use selected color
This commit is contained in:
Gigi
2025-10-05 04:12:31 +01:00
parent b59a295ad3
commit e4b6d1a122
5 changed files with 106 additions and 19 deletions

View File

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

View File

@@ -8,6 +8,14 @@ import { applyHighlightsToHTML } from '../utils/highlightMatching'
import { readingTime } from 'reading-time-estimator'
import { filterHighlightsByUrl } from '../utils/urlHelpers'
// Helper to convert hex color to RGB values
function hexToRgb(hex: string): string {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
: '255, 255, 0'
}
interface ContentPanelProps {
loading: boolean
title?: string
@@ -17,6 +25,7 @@ interface ContentPanelProps {
highlights?: Highlight[]
showUnderlines?: boolean
highlightStyle?: 'marker' | 'underline'
highlightColor?: string
onHighlightClick?: (highlightId: string) => void
selectedHighlightId?: string
}
@@ -30,6 +39,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
highlights = [],
showUnderlines = true,
highlightStyle = 'marker',
highlightColor = '#ffff00',
onHighlightClick,
selectedHighlightId
}) => {
@@ -151,8 +161,10 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
)
}
const highlightRgb = hexToRgb(highlightColor)
return (
<div className="reader">
<div className="reader" style={{ '--highlight-rgb': highlightRgb } as React.CSSProperties}>
{title && (
<div className="reader-header">
<h2 className="reader-title">{title}</h2>

View File

@@ -4,6 +4,14 @@ import { UserSettings } from '../services/settingsService'
import IconButton from './IconButton'
import { loadFont, getFontFamily } from '../utils/fontLoader'
// Helper to convert hex color to RGB values
function hexToRgb(hex: string): string {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result
? `${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}`
: '255, 255, 0'
}
interface SettingsProps {
settings: UserSettings
onSave: (settings: UserSettings) => Promise<void>
@@ -131,14 +139,38 @@ const Settings: React.FC<SettingsProps> = ({ settings, onSave, onClose }) => {
</div>
</div>
<div className="setting-group setting-inline">
<label>Highlight Color</label>
<div className="color-picker">
{[
{ name: 'Yellow', value: '#ffff00' },
{ name: 'Orange', value: '#ff9500' },
{ name: 'Pink', value: '#ff69b4' },
{ name: 'Green', value: '#00ff7f' },
{ name: 'Blue', value: '#4da6ff' },
{ name: 'Purple', value: '#b19cd9' }
].map(color => (
<button
key={color.value}
onClick={() => setLocalSettings({ ...localSettings, highlightColor: color.value })}
className={`color-swatch ${(localSettings.highlightColor || '#ffff00') === color.value ? 'active' : ''}`}
style={{ backgroundColor: color.value }}
title={color.name}
aria-label={`${color.name} highlight color`}
/>
))}
</div>
</div>
<div className="setting-preview">
<div className="preview-label">Preview</div>
<div
className="preview-content"
style={{
fontFamily: previewFontFamily,
fontSize: `${localSettings.fontSize || 16}px`
}}
fontSize: `${localSettings.fontSize || 16}px`,
'--highlight-rgb': hexToRgb(localSettings.highlightColor || '#ffff00')
} as React.CSSProperties}
>
<h3>The Quick Brown Fox</h3>
<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>

View File

@@ -1404,36 +1404,38 @@ body {
/* Inline content highlights - fluorescent marker style */
.content-highlight,
.content-highlight-marker {
background: rgba(255, 255, 0, 0.35);
--highlight-rgb: 255, 255, 0;
background: rgba(var(--highlight-rgb), 0.35);
padding: 0.125rem 0.25rem;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
border-radius: 2px;
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
box-shadow: 0 0 8px rgba(var(--highlight-rgb), 0.2);
}
.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);
background: rgba(var(--highlight-rgb), 0.5);
box-shadow: 0 0 12px rgba(var(--highlight-rgb), 0.3);
}
/* Underline style for highlights */
.content-highlight-underline {
--highlight-rgb: 255, 255, 0;
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-color: rgba(var(--highlight-rgb), 0.8);
text-decoration-thickness: 2px;
text-underline-offset: 2px;
}
.content-highlight-underline:hover {
text-decoration-color: rgba(255, 200, 0, 1);
text-decoration-color: rgba(var(--highlight-rgb), 1);
text-decoration-thickness: 3px;
}
@@ -1445,19 +1447,19 @@ body {
@keyframes highlight-pulse-animation {
0%, 100% {
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
box-shadow: 0 0 8px rgba(var(--highlight-rgb, 255, 255, 0), 0.2);
transform: scale(1);
}
25% {
box-shadow: 0 0 20px rgba(255, 255, 0, 0.6);
box-shadow: 0 0 20px rgba(var(--highlight-rgb, 255, 255, 0), 0.6);
transform: scale(1.02);
}
50% {
box-shadow: 0 0 8px rgba(255, 255, 0, 0.2);
box-shadow: 0 0 8px rgba(var(--highlight-rgb, 255, 255, 0), 0.2);
transform: scale(1);
}
75% {
box-shadow: 0 0 20px rgba(255, 255, 0, 0.6);
box-shadow: 0 0 20px rgba(var(--highlight-rgb, 255, 255, 0), 0.6);
transform: scale(1.02);
}
}
@@ -1482,22 +1484,22 @@ body {
@media (prefers-color-scheme: light) {
.content-highlight,
.content-highlight-marker {
background: rgba(255, 255, 0, 0.4);
box-shadow: 0 0 6px rgba(255, 255, 0, 0.15);
background: rgba(var(--highlight-rgb), 0.4);
box-shadow: 0 0 6px rgba(var(--highlight-rgb), 0.15);
}
.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);
background: rgba(var(--highlight-rgb), 0.55);
box-shadow: 0 0 10px rgba(var(--highlight-rgb), 0.25);
}
.content-highlight-underline {
text-decoration-color: rgba(255, 180, 0, 0.9);
text-decoration-color: rgba(var(--highlight-rgb), 0.9);
}
.content-highlight-underline:hover {
text-decoration-color: rgba(255, 180, 0, 1);
text-decoration-color: rgba(var(--highlight-rgb), 1);
}
.highlight-indicator {
@@ -1587,6 +1589,45 @@ body {
gap: 0.5rem;
}
.color-picker {
display: flex;
align-items: center;
gap: 0.5rem;
}
.color-swatch {
width: 2.5rem;
height: 2.5rem;
border: 2px solid #444;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.color-swatch:hover {
border-color: #888;
transform: scale(1.05);
}
.color-swatch.active {
border-color: #fff;
box-shadow: 0 0 0 2px #646cff;
transform: scale(1.1);
}
.color-swatch.active::after {
content: '✓';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #000;
font-size: 1rem;
font-weight: bold;
text-shadow: 0 0 2px #fff;
}
.font-size-btn {
min-width: 2.5rem;
height: 2.5rem;

View File

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