mirror of
https://github.com/dergigi/boris.git
synced 2025-12-26 11:04:24 +01:00
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:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface UserSettings {
|
||||
readingFont?: string
|
||||
fontSize?: number
|
||||
highlightStyle?: 'marker' | 'underline'
|
||||
highlightColor?: string
|
||||
}
|
||||
|
||||
export async function loadSettings(
|
||||
|
||||
Reference in New Issue
Block a user