Compare commits

...

22 Commits

Author SHA1 Message Date
Gigi
76f4588c85 chore: bump version to 0.6.16 2025-10-15 18:30:36 +02:00
Gigi
e163b92a7e fix: remove unused handleCancelDelete function
Removed handleCancelDelete as it's no longer needed after switching
from ConfirmDialog modal to inline confirmation
2025-10-15 18:30:15 +02:00
Gigi
11925a42b0 style: make trash icon red in delete confirmation
Change from CompactButton to regular button with explicit red color
styling so the trash icon inherits the red color (rgb(220 38 38))
2025-10-15 18:21:44 +02:00
Gigi
acf45530ca refactor: replace delete dialog with inline confirmation
Replace popup modal with inline confirmation UI:
- When delete is clicked, show red trash icon with 'Confirm?' text
- Clicking red trash icon again confirms deletion
- Confirmation appears to left of three-dot menu
- Click outside or reopen menu cancels confirmation
- Remove ConfirmDialog component dependency
2025-10-15 18:15:55 +02:00
Gigi
3792ad6abf refactor: move Highlight Style, Paragraph Alignment, and Default Highlight Visibility to top
Final order:
1. Highlight Style
2. Paragraph Alignment
3. Default Highlight Visibility
4. Reading Font + Font Size
5. My Highlights color
6. Friends Highlights color
7. Nostrverse Highlights color
8. Show highlights checkbox
9. Preview
2025-10-15 17:59:29 +02:00
Gigi
bf98b307e8 style: align setting buttons vertically with fixed label width
Add min-width: 220px to inline setting labels to create consistent
'tab stops' so buttons align vertically regardless of label length.
Remove constraint on mobile where settings stack vertically.
2025-10-15 17:57:33 +02:00
Gigi
d15392f41e refactor: reorder settings with Highlight Style and Paragraph Alignment above Default Highlight Visibility
Final order:
1. Reading Font + Font Size
2. My Highlights color
3. Friends Highlights color
4. Nostrverse Highlights color
5. Highlight Style
6. Paragraph Alignment
7. Default Highlight Visibility
8. Show highlights checkbox
9. Preview
2025-10-15 17:55:58 +02:00
Gigi
f26a024255 refactor: reorder Reading & Display settings
- Highlight Style (first)
- Paragraph Alignment (second)
- Reading Font + Font Size (third)

Better logical grouping with text styling before font selection
2025-10-15 17:54:08 +02:00
Gigi
bf9f894c0d refactor: improve delete dialog UI and simplify message
- Reduce verbose warning text to simple 'This will delete your highlight'
- Add proper CSS styling for confirm dialog with backdrop blur
- Center-aligned text and circular icon with color-coded background
- Modern button styling with proper hover states
- Full-width buttons in action row
- Theme-aware colors using CSS variables
2025-10-15 17:53:26 +02:00
Gigi
53a7b7d1c5 docs: update CHANGELOG.md for v0.6.15 2025-10-15 17:51:38 +02:00
Gigi
a12a883cc6 chore: bump version to 0.6.15 2025-10-15 17:50:47 +02:00
Gigi
0cf076b010 chore: change default paragraph alignment to justify 2025-10-15 17:45:10 +02:00
Gigi
e2c712033f feat: add paragraph alignment setting
- Add paragraphAlignment setting (left/justify) to UserSettings interface
- Add UI control with icon buttons in ReadingDisplaySettings
- Apply alignment via CSS variable to reader content and preview
- Default to left-aligned to maintain current behavior
- Keep headings always left-aligned for better readability
2025-10-15 17:43:31 +02:00
Gigi
e38237ca8e fix: resolve linter errors for unused variables 2025-10-15 17:39:22 +02:00
Gigi
1fff44fc6c chore: update button label from 'Open on Nostr' to 'Open with njump' 2025-10-15 17:37:38 +02:00
Gigi
4e50073e07 feat: update gateway config to use nostr.at and define ants.sh as search portal 2025-10-15 17:31:39 +02:00
Gigi
0ce64fe83f feat: open three-dot menus upward when insufficient space below 2025-10-15 17:28:44 +02:00
Gigi
ef848aa93e feat: update external article menu with Share (Boris link) and Search options 2025-10-15 17:25:29 +02:00
Gigi
67b287d75d feat: add Search option to article menu for ants.sh portal 2025-10-15 17:20:59 +02:00
Gigi
b795dfd2c6 feat: add Copy Link and Copy Original options to article menu 2025-10-15 17:19:20 +02:00
Gigi
c68d855983 feat: add Share and Share Original options to article menu 2025-10-15 17:18:36 +02:00
Gigi
fb1c19e64b docs: update CHANGELOG.md for v0.6.14 2025-10-15 16:30:11 +02:00
13 changed files with 433 additions and 109 deletions

View File

@@ -7,6 +7,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.6.15] - 2025-10-15
### Added
- Paragraph alignment setting with left-aligned and justified text options
- Icon buttons in Reading & Display settings for switching alignment
- CSS variable system for applying alignment to reader content
- Real-time preview of alignment changes in settings
- Headings remain left-aligned for optimal readability
### Changed
- Default paragraph alignment changed to justified for improved reading experience
- Applies to paragraphs, list items, divs, and blockquotes
- Settings stored and synced via Nostr (NIP-78)
## [0.6.14] - 2025-10-15
### Added
- Support for bookmark sets (NIP-51 kind:30003)
- Bookmark sets now display alongside regular bookmark lists
- Properly handles AddressPointer bookmarks for long-form articles
- Content type icons for bookmarks
- Article, video, web, and image icons to indicate bookmark content type
- Camera icon for image bookmarks
- Sticky note icon for text-only bookmarks without URLs
- Bookmark grouping and sections
- Grouped sections in sidebar and `/me` reading-list
- Web bookmarks, default bookmarks, and legacy bookmarks in separate sections
- Grouping and sorting helpers for organizing bookmark sections
- Adaptive text color for publication date over hero images
- Automatically detects image brightness and adjusts text color
- Improved contrast for better readability
### Changed
- Renamed "Amethyst-style bookmarks" to "Old Bookmarks (Legacy)"
- Hide cover images in compact view for cleaner layout
- Support button improvements
- Moved to bottom-left of bookmarks bar
- Changed icon from lightning bolt to heart (orange color)
- Left-aligned support button, right-aligned view mode buttons
- Section headings improved with better typography (removed counts)
- Icon changed from book to file-lines for default bookmarks
- Use regular (outlined) icon variants for lighter, more refined appearance
- Add bookmark button moved to web bookmarks section
- Empty state messages replaced with loading spinners
- Section dividers made more subtle
- Simplified bookmark filtering to only exclude empty content
### Fixed
- Removed borders from compact bookmark cards for cleaner look
- Removed duplicate type indicator icons from bookmark cards
- Reduced section heading bottom padding for better spacing
- Aligned add bookmark button with section heading
- Removed redundant loading spinner above tabs
- Resolved linter and type errors
- Include kind:30003 in default bookmark list detection
- Removed text shadows from publication date for cleaner look
- Improved shadow contrast without background overlay
- Corrected async handling in adaptive color detection
- Corrected FastAverageColor import to use named export
- Section heading styles now properly override with `!important`
- Removed unused articleImage prop from CompactView
## [0.6.13] - 2025-10-15
### Added
@@ -1415,7 +1482,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimize relay usage following applesauce-relay best practices
- Use applesauce-react event models for better profile handling
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.13...HEAD
[Unreleased]: https://github.com/dergigi/boris/compare/v0.6.15...HEAD
[0.6.15]: https://github.com/dergigi/boris/compare/v0.6.14...v0.6.15
[0.6.14]: https://github.com/dergigi/boris/compare/v0.6.13...v0.6.14
[0.6.13]: https://github.com/dergigi/boris/compare/v0.6.12...v0.6.13
[0.6.12]: https://github.com/dergigi/boris/compare/v0.6.11...v0.6.12
[0.6.11]: https://github.com/dergigi/boris/compare/v0.6.10...v0.6.11

View File

@@ -1,6 +1,6 @@
{
"name": "boris",
"version": "0.6.14",
"version": "0.6.16",
"description": "A minimal nostr client for bookmark management",
"homepage": "https://read.withboris.com/",
"type": "module",

View File

@@ -142,7 +142,8 @@ export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onS
}
if (viewMode === 'compact') {
const { articleImage: _articleImage, ...compactProps } = sharedProps
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const { articleImage, ...compactProps } = sharedProps
return <CompactView {...compactProps} />
}

View File

@@ -6,10 +6,10 @@ import rehypeRaw from 'rehype-raw'
import rehypePrism from 'rehype-prism-plus'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import 'prismjs/themes/prism-tomorrow.css'
import { faSpinner, faCheckCircle, faEllipsisH, faExternalLinkAlt, faMobileAlt, faCopy, faShare } from '@fortawesome/free-solid-svg-icons'
import { faSpinner, faCheckCircle, faEllipsisH, faExternalLinkAlt, faMobileAlt, faCopy, faShare, faSearch } from '@fortawesome/free-solid-svg-icons'
import { ContentSkeleton } from './Skeletons'
import { nip19 } from 'nostr-tools'
import { getNostrUrl } from '../config/nostrGateways'
import { getNostrUrl, getSearchUrl } from '../config/nostrGateways'
import { RELAYS } from '../config/relays'
import { RelayPool } from 'applesauce-relay'
import { IAccount } from 'applesauce-accounts'
@@ -100,6 +100,9 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const [showArticleMenu, setShowArticleMenu] = useState(false)
const [showVideoMenu, setShowVideoMenu] = useState(false)
const [showExternalMenu, setShowExternalMenu] = useState(false)
const [articleMenuOpenUpward, setArticleMenuOpenUpward] = useState(false)
const [videoMenuOpenUpward, setVideoMenuOpenUpward] = useState(false)
const [externalMenuOpenUpward, setExternalMenuOpenUpward] = useState(false)
const articleMenuRef = useRef<HTMLDivElement>(null)
const videoMenuRef = useRef<HTMLDivElement>(null)
const externalMenuRef = useRef<HTMLDivElement>(null)
@@ -161,6 +164,35 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
}
}, [showArticleMenu, showVideoMenu, showExternalMenu])
// Check available space and position menu upward if needed
useEffect(() => {
const checkMenuPosition = (menuRef: React.RefObject<HTMLDivElement>, setOpenUpward: (value: boolean) => void) => {
if (!menuRef.current) return
const menuWrapper = menuRef.current
const menuElement = menuWrapper.querySelector('.article-menu') as HTMLElement
if (!menuElement) return
const rect = menuWrapper.getBoundingClientRect()
const viewportHeight = window.innerHeight
const spaceBelow = viewportHeight - rect.bottom
const menuHeight = menuElement.offsetHeight || 300 // estimate if not rendered yet
// Open upward if there's not enough space below (with 20px buffer)
setOpenUpward(spaceBelow < menuHeight + 20 && rect.top > menuHeight)
}
if (showArticleMenu) {
checkMenuPosition(articleMenuRef, setArticleMenuOpenUpward)
}
if (showVideoMenu) {
checkMenuPosition(videoMenuRef, setVideoMenuOpenUpward)
}
if (showExternalMenu) {
checkMenuPosition(externalMenuRef, setExternalMenuOpenUpward)
}
}, [showArticleMenu, showVideoMenu, showExternalMenu])
const readingStats = useMemo(() => {
const content = markdown || html || ''
if (!content) return null
@@ -218,9 +250,15 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
relays: relayHints
})
// Check for source URL in 'r' tags
const sourceUrl = currentArticle.tags.find(t => t[0] === 'r')?.[1]
return {
portal: getNostrUrl(naddr),
native: `nostr:${naddr}`
native: `nostr:${naddr}`,
naddr,
sourceUrl,
borisUrl: `${window.location.origin}/a/${naddr}`
}
}
@@ -245,6 +283,73 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
}
setShowArticleMenu(false)
}
const handleShareBoris = async () => {
try {
if (!articleLinks) return
if ((navigator as { share?: (d: { title?: string; url?: string }) => Promise<void> }).share) {
await (navigator as { share: (d: { title?: string; url?: string }) => Promise<void> }).share({
title: title || 'Article',
url: articleLinks.borisUrl
})
} else {
await navigator.clipboard.writeText(articleLinks.borisUrl)
}
} catch (e) {
console.warn('Share failed', e)
} finally {
setShowArticleMenu(false)
}
}
const handleShareOriginal = async () => {
try {
if (!articleLinks?.sourceUrl) return
if ((navigator as { share?: (d: { title?: string; url?: string }) => Promise<void> }).share) {
await (navigator as { share: (d: { title?: string; url?: string }) => Promise<void> }).share({
title: title || 'Article',
url: articleLinks.sourceUrl
})
} else {
await navigator.clipboard.writeText(articleLinks.sourceUrl)
}
} catch (e) {
console.warn('Share failed', e)
} finally {
setShowArticleMenu(false)
}
}
const handleCopyBoris = async () => {
try {
if (!articleLinks) return
await navigator.clipboard.writeText(articleLinks.borisUrl)
} catch (e) {
console.warn('Copy failed', e)
} finally {
setShowArticleMenu(false)
}
}
const handleCopyOriginal = async () => {
try {
if (!articleLinks?.sourceUrl) return
await navigator.clipboard.writeText(articleLinks.sourceUrl)
} catch (e) {
console.warn('Copy failed', e)
} finally {
setShowArticleMenu(false)
}
}
const handleOpenSearch = () => {
if (articleLinks) {
window.open(getSearchUrl(articleLinks.naddr), '_blank', 'noopener,noreferrer')
}
setShowArticleMenu(false)
}
// Video actions
const handleOpenVideoExternal = () => {
@@ -307,10 +412,16 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
const handleShareExternalUrl = async () => {
try {
if (selectedUrl && (navigator as { share?: (d: { title?: string; url?: string }) => Promise<void> }).share) {
await (navigator as { share: (d: { title?: string; url?: string }) => Promise<void> }).share({ title: title || 'Article', url: selectedUrl })
} else if (selectedUrl) {
await navigator.clipboard.writeText(selectedUrl)
if (!selectedUrl) return
const borisUrl = `${window.location.origin}/r/${encodeURIComponent(selectedUrl)}`
if ((navigator as { share?: (d: { title?: string; url?: string }) => Promise<void> }).share) {
await (navigator as { share: (d: { title?: string; url?: string }) => Promise<void> }).share({
title: title || 'Article',
url: borisUrl
})
} else {
await navigator.clipboard.writeText(borisUrl)
}
} catch (e) {
console.warn('Share failed', e)
@@ -318,6 +429,13 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
setShowExternalMenu(false)
}
}
const handleSearchExternalUrl = () => {
if (selectedUrl) {
window.open(getSearchUrl(selectedUrl), '_blank', 'noopener,noreferrer')
}
setShowExternalMenu(false)
}
// Check if article is already marked as read when URL/article changes
useEffect(() => {
@@ -501,7 +619,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
<FontAwesomeIcon icon={faEllipsisH} />
</button>
{showVideoMenu && (
<div className="article-menu">
<div className={`article-menu ${videoMenuOpenUpward ? 'open-upward' : ''}`}>
<button className="article-menu-item" onClick={handleOpenVideoExternal}>
<FontAwesomeIcon icon={faExternalLinkAlt} />
<span>Open Link</span>
@@ -582,13 +700,13 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
</button>
{showExternalMenu && (
<div className="article-menu">
<div className={`article-menu ${externalMenuOpenUpward ? 'open-upward' : ''}`}>
<button
className="article-menu-item"
onClick={handleOpenExternalUrl}
onClick={handleShareExternalUrl}
>
<FontAwesomeIcon icon={faExternalLinkAlt} />
<span>Open Original URL</span>
<FontAwesomeIcon icon={faShare} />
<span>Share</span>
</button>
<button
className="article-menu-item"
@@ -599,10 +717,17 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
</button>
<button
className="article-menu-item"
onClick={handleShareExternalUrl}
onClick={handleOpenExternalUrl}
>
<FontAwesomeIcon icon={faShare} />
<span>Share</span>
<FontAwesomeIcon icon={faExternalLinkAlt} />
<span>Open Original</span>
</button>
<button
className="article-menu-item"
onClick={handleSearchExternalUrl}
>
<FontAwesomeIcon icon={faSearch} />
<span>Search</span>
</button>
</div>
)}
@@ -623,13 +748,52 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
</button>
{showArticleMenu && (
<div className="article-menu">
<div className={`article-menu ${articleMenuOpenUpward ? 'open-upward' : ''}`}>
<button
className="article-menu-item"
onClick={handleShareBoris}
>
<FontAwesomeIcon icon={faShare} />
<span>Share</span>
</button>
{articleLinks.sourceUrl && (
<button
className="article-menu-item"
onClick={handleShareOriginal}
>
<FontAwesomeIcon icon={faShare} />
<span>Share Original</span>
</button>
)}
<button
className="article-menu-item"
onClick={handleCopyBoris}
>
<FontAwesomeIcon icon={faCopy} />
<span>Copy Link</span>
</button>
{articleLinks.sourceUrl && (
<button
className="article-menu-item"
onClick={handleCopyOriginal}
>
<FontAwesomeIcon icon={faCopy} />
<span>Copy Original</span>
</button>
)}
<button
className="article-menu-item"
onClick={handleOpenSearch}
>
<FontAwesomeIcon icon={faSearch} />
<span>Search</span>
</button>
<button
className="article-menu-item"
onClick={handleOpenPortal}
>
<FontAwesomeIcon icon={faExternalLinkAlt} />
<span>Open on Nostr</span>
<span>Open with njump</span>
</button>
<button
className="article-menu-item"

View File

@@ -13,7 +13,6 @@ import { areAllRelaysLocal } from '../utils/helpers'
import { nip19 } from 'nostr-tools'
import { formatDateCompact } from '../utils/bookmarkUtils'
import { createDeletionRequest } from '../services/deletionService'
import ConfirmDialog from './ConfirmDialog'
import { getNostrUrl } from '../config/nostrGateways'
import CompactButton from './CompactButton'
import { HighlightCitation } from './HighlightCitation'
@@ -257,21 +256,22 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
}
}, [isSelected])
// Close menu when clicking outside
// Close menu and reset delete confirm when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setShowMenu(false)
setShowDeleteConfirm(false)
}
}
if (showMenu) {
if (showMenu || showDeleteConfirm) {
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}
}, [showMenu])
}, [showMenu, showDeleteConfirm])
const handleItemClick = () => {
if (onHighlightClick) {
@@ -434,12 +434,12 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
}
}
const handleCancelDelete = () => {
setShowDeleteConfirm(false)
}
const handleMenuToggle = (e: React.MouseEvent) => {
e.stopPropagation()
// Reset delete confirm state when opening/closing menu
if (!showMenu) {
setShowDeleteConfirm(false)
}
setShowMenu(!showMenu)
}
@@ -461,6 +461,11 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
setShowDeleteConfirm(true)
}
const handleConfirmDeleteClick = (e: React.MouseEvent) => {
e.stopPropagation()
handleConfirmDelete()
}
return (
<>
<div
@@ -533,6 +538,33 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
</div>
<div className="highlight-menu-wrapper" ref={menuRef}>
{showDeleteConfirm && canDelete && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginRight: '0.5rem' }}>
<span style={{ fontSize: '0.875rem', color: 'rgb(220 38 38)', fontWeight: 500 }}>Confirm?</span>
<button
onClick={handleConfirmDeleteClick}
disabled={isDeleting}
title="Confirm deletion"
style={{
color: 'rgb(220 38 38)',
background: 'rgba(220, 38, 38, 0.1)',
border: '1px solid rgb(220 38 38)',
borderRadius: '4px',
padding: '0.375rem',
cursor: isDeleting ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minWidth: '33px',
minHeight: '33px',
transition: 'all 0.2s'
}}
>
<FontAwesomeIcon icon={isDeleting ? faSpinner : faTrash} spin={isDeleting} />
</button>
</div>
)}
<CompactButton
icon={faEllipsisH}
onClick={handleMenuToggle}
@@ -546,7 +578,7 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
onClick={handleOpenPortal}
>
<FontAwesomeIcon icon={faExternalLinkAlt} />
<span>Open on Nostr</span>
<span>Open with njump</span>
</button>
<button
className="highlight-menu-item"
@@ -571,17 +603,6 @@ export const HighlightItem: React.FC<HighlightItemProps> = ({
</div>
</div>
</div>
<ConfirmDialog
isOpen={showDeleteConfirm}
title="Delete Highlight?"
message="This will request deletion of your highlight. It may still be visible on some relays that don't honor deletion requests."
confirmText="Delete"
cancelText="Cancel"
variant="danger"
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
</>
)
}

View File

@@ -35,6 +35,7 @@ const DEFAULT_SETTINGS: UserSettings = {
zapSplitAuthorWeight: 50,
useLocalRelayAsCache: true,
rebroadcastToAllRelays: false,
paragraphAlignment: 'justify',
}
interface SettingsProps {

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { faHighlighter, faUnderline, faNetworkWired, faUserGroup, faUser } from '@fortawesome/free-solid-svg-icons'
import { faHighlighter, faUnderline, faNetworkWired, faUserGroup, faUser, faAlignLeft, faAlignJustify } from '@fortawesome/free-solid-svg-icons'
import { UserSettings } from '../../services/settingsService'
import IconButton from '../IconButton'
import ColorPicker from '../ColorPicker'
@@ -19,35 +19,6 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
<div className="settings-section">
<h3 className="section-title">Reading & Display</h3>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div className="setting-group setting-inline" style={{ flex: '1 1 auto', minWidth: '200px' }}>
<label htmlFor="readingFont">Reading Font</label>
<div className="setting-control">
<FontSelector
value={settings.readingFont || 'source-serif-4'}
onChange={(font) => onUpdate({ readingFont: font })}
/>
</div>
</div>
<div className="setting-group setting-inline" style={{ flex: '0 1 auto' }}>
<label>Font Size</label>
<div className="setting-buttons">
{[16, 18, 21, 24, 28, 32].map(size => (
<button
key={size}
onClick={() => onUpdate({ fontSize: size })}
className={`font-size-btn ${(settings.fontSize || 21) === size ? 'active' : ''}`}
title={`${size}px`}
style={{ fontSize: `${size - 2}px` }}
>
A
</button>
))}
</div>
</div>
</div>
<div className="setting-group setting-inline">
<label>Highlight Style</label>
<div className="setting-buttons">
@@ -69,31 +40,21 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
</div>
<div className="setting-group setting-inline">
<label className="setting-label">My Highlights</label>
<div className="setting-control">
<ColorPicker
selectedColor={settings.highlightColorMine || '#fde047'}
onColorChange={(color) => onUpdate({ highlightColorMine: color })}
<label>Paragraph Alignment</label>
<div className="setting-buttons">
<IconButton
icon={faAlignLeft}
onClick={() => onUpdate({ paragraphAlignment: 'left' })}
title="Left aligned"
ariaLabel="Left aligned"
variant={settings.paragraphAlignment === 'left' ? 'primary' : 'ghost'}
/>
</div>
</div>
<div className="setting-group setting-inline">
<label className="setting-label">Friends Highlights</label>
<div className="setting-control">
<ColorPicker
selectedColor={settings.highlightColorFriends || '#f97316'}
onColorChange={(color) => onUpdate({ highlightColorFriends: color })}
/>
</div>
</div>
<div className="setting-group setting-inline">
<label className="setting-label">Nostrverse Highlights</label>
<div className="setting-control">
<ColorPicker
selectedColor={settings.highlightColorNostrverse || '#9333ea'}
onColorChange={(color) => onUpdate({ highlightColorNostrverse: color })}
<IconButton
icon={faAlignJustify}
onClick={() => onUpdate({ paragraphAlignment: 'justify' })}
title="Justified"
ariaLabel="Justified"
variant={(settings.paragraphAlignment || 'justify') === 'justify' ? 'primary' : 'ghost'}
/>
</div>
</div>
@@ -137,6 +98,65 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
</div>
</div>
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
<div className="setting-group setting-inline" style={{ flex: '1 1 auto', minWidth: '200px' }}>
<label htmlFor="readingFont">Reading Font</label>
<div className="setting-control">
<FontSelector
value={settings.readingFont || 'source-serif-4'}
onChange={(font) => onUpdate({ readingFont: font })}
/>
</div>
</div>
<div className="setting-group setting-inline" style={{ flex: '0 1 auto' }}>
<label>Font Size</label>
<div className="setting-buttons">
{[16, 18, 21, 24, 28, 32].map(size => (
<button
key={size}
onClick={() => onUpdate({ fontSize: size })}
className={`font-size-btn ${(settings.fontSize || 21) === size ? 'active' : ''}`}
title={`${size}px`}
style={{ fontSize: `${size - 2}px` }}
>
A
</button>
))}
</div>
</div>
</div>
<div className="setting-group setting-inline">
<label className="setting-label">My Highlights</label>
<div className="setting-control">
<ColorPicker
selectedColor={settings.highlightColorMine || '#fde047'}
onColorChange={(color) => onUpdate({ highlightColorMine: color })}
/>
</div>
</div>
<div className="setting-group setting-inline">
<label className="setting-label">Friends Highlights</label>
<div className="setting-control">
<ColorPicker
selectedColor={settings.highlightColorFriends || '#f97316'}
onColorChange={(color) => onUpdate({ highlightColorFriends: color })}
/>
</div>
</div>
<div className="setting-group setting-inline">
<label className="setting-label">Nostrverse Highlights</label>
<div className="setting-control">
<ColorPicker
selectedColor={settings.highlightColorNostrverse || '#9333ea'}
onColorChange={(color) => onUpdate({ highlightColorNostrverse: color })}
/>
</div>
</div>
<div className="setting-group">
<label htmlFor="showHighlights" className="checkbox-label">
<input
@@ -157,7 +177,8 @@ const ReadingDisplaySettings: React.FC<ReadingDisplaySettingsProps> = ({ setting
style={{
fontFamily: previewFontFamily,
fontSize: `${settings.fontSize || 21}px`,
'--highlight-rgb': hexToRgb(settings.highlightColor || '#ffff00')
'--highlight-rgb': hexToRgb(settings.highlightColor || '#ffff00'),
'--paragraph-alignment': settings.paragraphAlignment || 'justify'
} as React.CSSProperties}
>
<h3>The Quick Brown Fox</h3>

View File

@@ -2,20 +2,21 @@
* Nostr gateway URLs for viewing events and profiles on the web
*/
export const NOSTR_GATEWAY = 'https://ants.sh' as const
export const NOSTR_GATEWAY = 'https://nostr.at' as const
export const SEARCH_PORTAL = 'https://ants.sh' as const
/**
* Get a profile URL on the gateway
*/
export function getProfileUrl(npub: string): string {
return `${NOSTR_GATEWAY}/p/${npub}`
return `${NOSTR_GATEWAY}/${npub}`
}
/**
* Get an event URL on the gateway
*/
export function getEventUrl(nevent: string): string {
return `${NOSTR_GATEWAY}/e/${nevent}`
return `${NOSTR_GATEWAY}/${nevent}`
}
/**
@@ -23,12 +24,14 @@ export function getEventUrl(nevent: string): string {
* Automatically detects if it's a profile (npub/nprofile) or event (note/nevent/naddr)
*/
export function getNostrUrl(identifier: string): string {
// Check the prefix to determine if it's a profile or event
if (identifier.startsWith('npub') || identifier.startsWith('nprofile')) {
return `${NOSTR_GATEWAY}/p/${identifier}`
}
// Everything else (note, nevent, naddr) goes to /e/
return `${NOSTR_GATEWAY}/e/${identifier}`
// nostr.at uses simple /{identifier} format for all types
return `${NOSTR_GATEWAY}/${identifier}`
}
/**
* Get a search portal URL with a query
*/
export function getSearchUrl(query: string): string {
return `${SEARCH_PORTAL}/?q=${encodeURIComponent(query)}`
}

View File

@@ -73,6 +73,9 @@ export function useSettings({ relayPool, eventStore, pubkey, accountManager }: U
root.setProperty('--highlight-color-friends', settings.highlightColorFriends || '#f97316')
root.setProperty('--highlight-color-nostrverse', settings.highlightColorNostrverse || '#9333ea')
// Set paragraph alignment
root.setProperty('--paragraph-alignment', settings.paragraphAlignment || 'justify')
console.log('✅ All styles applied')
}

View File

@@ -52,6 +52,8 @@ export interface UserSettings {
theme?: 'dark' | 'light' | 'system' // default: system
darkColorTheme?: 'black' | 'midnight' | 'charcoal' // default: midnight
lightColorTheme?: 'paper-white' | 'sepia' | 'ivory' // default: sepia
// Reading settings
paragraphAlignment?: 'left' | 'justify' // default: justify
}
export async function loadSettings(

View File

@@ -3,7 +3,7 @@
.setting-group.setting-inline { display: flex; align-items: center; gap: 1rem; }
.setting-label { text-align: left; flex: 1; }
.setting-control { display: flex; justify-content: flex-end; align-items: center; }
.setting-group.setting-inline label { margin-bottom: 0; }
.setting-group.setting-inline label { margin-bottom: 0; min-width: 220px; }
.setting-group label { display: block; margin-bottom: 0.5rem; color: var(--color-text); font-weight: 500; text-align: left; }
.setting-buttons { display: flex; align-items: center; gap: 0.5rem; }
.color-picker { display: flex; align-items: center; gap: 0.5rem; }
@@ -41,6 +41,7 @@
.preview-content p {
margin: 0.75rem 0;
word-wrap: break-word;
text-align: var(--paragraph-alignment, justify);
}
.setting-select { width: 100%; padding: 0.5rem; background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 4px; color: var(--color-text); font-size: 1rem; }
.setting-inline .setting-select { width: auto; min-width: 200px; flex: 1; }
@@ -58,6 +59,10 @@
gap: 0.75rem;
}
.setting-group.setting-inline label {
min-width: unset;
}
.setting-inline .setting-select {
width: 100%;
min-width: unset;

View File

@@ -25,3 +25,23 @@
.btn-primary:hover:not(:disabled) { background: var(--color-primary-hover); }
.btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
/* Confirm Dialog */
.confirm-dialog-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 10000; padding: 1rem; }
.confirm-dialog { background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-radius: 12px; max-width: 400px; width: 100%; padding: 1.5rem; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); }
.confirm-dialog-icon { display: flex; align-items: center; justify-content: center; width: 48px; height: 48px; border-radius: 50%; margin: 0 auto 1rem; font-size: 1.5rem; }
.confirm-dialog-icon.danger { background: rgba(220, 38, 38, 0.1); color: rgb(220 38 38); }
.confirm-dialog-icon.warning { background: rgba(251, 191, 36, 0.1); color: rgb(251 191 36); }
.confirm-dialog-icon.info { background: rgba(59, 130, 246, 0.1); color: rgb(59 130 246); }
.confirm-dialog-title { margin: 0 0 0.5rem 0; font-size: 1.25rem; font-weight: 600; color: var(--color-text); text-align: center; }
.confirm-dialog-message { margin: 0 0 1.5rem 0; font-size: 0.9rem; color: var(--color-text-secondary); text-align: center; line-height: 1.5; }
.confirm-dialog-actions { display: flex; gap: 0.75rem; }
.confirm-dialog-btn { flex: 1; padding: 0.75rem 1rem; border: none; border-radius: 8px; font-size: 0.9rem; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.confirm-dialog-btn.cancel { background: var(--color-bg); border: 1px solid var(--color-border); color: var(--color-text); }
.confirm-dialog-btn.cancel:hover { background: var(--color-border); }
.confirm-dialog-btn.confirm.danger { background: rgb(220 38 38); color: white; }
.confirm-dialog-btn.confirm.danger:hover { background: rgb(185 28 28); }
.confirm-dialog-btn.confirm.warning { background: rgb(251 191 36); color: rgb(17 24 39); }
.confirm-dialog-btn.confirm.warning:hover { background: rgb(245 158 11); }
.confirm-dialog-btn.confirm.info { background: rgb(59 130 246); color: white; }
.confirm-dialog-btn.confirm.info:hover { background: rgb(37 99 235); }

View File

@@ -37,9 +37,22 @@
.highlight-indicator svg { font-size: 0.875rem; }
.reader-html { color: var(--color-text); line-height: 1.6; word-wrap: break-word; overflow-wrap: break-word; word-break: break-word; font-family: var(--reading-font); font-size: var(--reading-font-size); }
.reader-markdown { color: var(--color-text); line-height: 1.7; font-family: var(--reading-font); font-size: var(--reading-font-size); }
/* Ensure content is left-aligned even if source markup uses center */
.reader .reader-html *, .reader .reader-markdown * { text-align: left !important; font-family: inherit !important; }
.reader center, .reader [align="center"] { text-align: left !important; }
/* Ensure font inheritance */
.reader .reader-html *, .reader .reader-markdown * { font-family: inherit !important; }
/* Apply paragraph alignment from settings */
.reader .reader-html p,
.reader .reader-markdown p,
.reader .reader-html div,
.reader .reader-markdown div,
.reader .reader-html li,
.reader .reader-markdown li,
.reader .reader-html blockquote,
.reader .reader-markdown blockquote { text-align: var(--paragraph-alignment, justify); }
/* Override centered content with user preference */
.reader center, .reader [align="center"] { text-align: var(--paragraph-alignment, justify) !important; }
/* Keep headings left-aligned */
.reader .reader-html h1, .reader .reader-html h2, .reader .reader-html h3, .reader .reader-html h4, .reader .reader-html h5, .reader .reader-html h6,
.reader .reader-markdown h1, .reader .reader-markdown h2, .reader .reader-markdown h3, .reader .reader-markdown h4, .reader .reader-markdown h5, .reader .reader-markdown h6 { text-align: left !important; }
/* Tame images from external content */
.reader .reader-html img, .reader .reader-markdown img { max-width: 100%; max-height: 70vh; height: auto; width: auto; display: block; margin: 0.75rem 0; border-radius: 6px; }
/* Headlines with Tailwind typography */
@@ -192,6 +205,7 @@
.article-menu-btn { background: none; border: none; color: var(--color-text-secondary); cursor: pointer; padding: 0.5rem 0.75rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s ease; border-radius: 6px; }
.article-menu-btn:hover { color: var(--color-primary); background: rgba(99, 102, 241, 0.1); }
.article-menu { position: absolute; right: 0; top: calc(100% + 4px); background: var(--color-bg-elevated); border: 1px solid var(--color-border-subtle); border-radius: 6px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; min-width: 180px; overflow: hidden; }
.article-menu.open-upward { top: auto; bottom: calc(100% + 4px); }
.article-menu-item { width: 100%; background: none; border: none; color: var(--color-text); padding: 0.75rem 1rem; font-size: 0.875rem; display: flex; align-items: center; gap: 0.75rem; cursor: pointer; transition: all 0.15s ease; text-align: left; white-space: nowrap; }
.article-menu-item:hover { background: rgba(99, 102, 241, 0.15); color: var(--color-text); }
.article-menu-item svg { font-size: 0.875rem; flex-shrink: 0; }