Compare commits

..

10 Commits

Author SHA1 Message Date
Gigi
5a67be8096 docs: update CHANGELOG for v0.4.1 release 2025-10-10 21:46:30 +01:00
Gigi
9a929a6be4 chore: bump version to 0.4.1 2025-10-10 21:45:41 +01:00
Gigi
e0ca010026 feat: improve hero image rendering with zoom-to-fit on mobile 2025-10-10 21:44:51 +01:00
Gigi
8bd5d7aadf fix: move long article summaries below image on mobile to prevent overlay issues 2025-10-10 21:43:55 +01:00
Gigi
9115c38cde fix: improve article summary display on mobile devices 2025-10-10 21:40:15 +01:00
Gigi
0c7c1d54d9 feat: add nstart.me onboarding link for new users 2025-10-10 21:26:06 +01:00
Gigi
d529d83eb8 fix: add touch event support for highlight creation on mobile 2025-10-10 21:24:46 +01:00
Gigi
a3127c7836 docs: update CHANGELOG for v0.4.0 release 2025-10-10 18:07:57 +01:00
Gigi
4d5fe1f425 chore: bump version to 0.4.0 2025-10-10 18:07:06 +01:00
Gigi
c7a4de9786 Merge pull request #1 from dergigi/mobile
Add mobile responsive design
2025-10-10 18:04:54 +01:00
8 changed files with 128 additions and 33 deletions

View File

@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.4.1] - 2025-10-10
### Fixed
- Long article summaries overlapping with hero image content on mobile devices
- Article summary now moves below hero image on mobile when longer than 150 characters
- Article summary line clamp reduced from 3 to 2 lines on mobile for better space utilization
### Changed
- Hero image rendering on mobile now uses zoom-to-fit approach with viewport-based sizing
- Hero image height on mobile set to 50vh (constrained between 280px-400px)
- Improved image cropping with center positioning for better visual presentation
- Optimized reader header overlay padding and title sizing on mobile
## [0.4.0] - 2025-10-10
### Added ### Added
- Mobile-responsive design with overlay sidebar drawer - Mobile-responsive design with overlay sidebar drawer
- Media query hooks for responsive behavior (`useIsMobile`, `useIsTablet`, `useIsCoarsePointer`) - Media query hooks for responsive behavior (`useIsMobile`, `useIsTablet`, `useIsCoarsePointer`)
@@ -19,12 +34,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Mobile-optimized modals (full-screen sheet style) - Mobile-optimized modals (full-screen sheet style)
- Mobile-optimized toast notifications (bottom position) - Mobile-optimized toast notifications (bottom position)
- Dynamic viewport height support (100dvh) - Dynamic viewport height support (100dvh)
- Mobile highlights panel as overlay with toggle button
### Changed ### Changed
- Sidebar now displays as overlay drawer on mobile (≤768px) - Sidebar now displays as overlay drawer on mobile (≤768px)
- Highlights panel hidden on mobile for better content focus - Highlights panel hidden on mobile for better content focus
- Sidebar auto-closes when selecting content on mobile - Sidebar auto-closes when selecting content on mobile
- Hover effects disabled on touch devices - Hover effects disabled on touch devices
- Replace hamburger icon with bookmark icon on mobile
### Fixed
- Ensure bookmarks container fills mobile sidepane properly
- Restore desktop grid layout for highlights panel
- Improve empty state and loading visibility in mobile sidepanes
- Add flex properties to mobile bookmark containers for proper filling
- Force bookmarks pane expanded on mobile and ensure highlights pane sits above content on desktop
- Reduce mobile backdrop opacity and ensure sidepanes appear above it
- Replace any type with proper bookmark interface for linter compliance
## [0.3.8] - 2025-10-10 ## [0.3.8] - 2025-10-10
@@ -564,6 +590,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optimize relay usage following applesauce-relay best practices - Optimize relay usage following applesauce-relay best practices
- Use applesauce-react event models for better profile handling - Use applesauce-react event models for better profile handling
[0.4.0]: https://github.com/dergigi/boris/compare/v0.3.8...v0.4.0
[0.3.8]: https://github.com/dergigi/boris/compare/v0.3.7...v0.3.8
[0.3.7]: https://github.com/dergigi/boris/compare/v0.3.6...v0.3.7
[0.3.6]: https://github.com/dergigi/boris/compare/v0.3.5...v0.3.6 [0.3.6]: https://github.com/dergigi/boris/compare/v0.3.5...v0.3.6
[0.3.5]: https://github.com/dergigi/boris/compare/v0.3.4...v0.3.5 [0.3.5]: https://github.com/dergigi/boris/compare/v0.3.4...v0.3.5
[0.3.4]: https://github.com/dergigi/boris/compare/v0.3.3...v0.3.4 [0.3.4]: https://github.com/dergigi/boris/compare/v0.3.3...v0.3.4

View File

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

View File

@@ -119,6 +119,7 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
<div className="empty-state"> <div className="empty-state">
<p>No bookmarks found.</p> <p>No bookmarks found.</p>
<p>Add bookmarks using your nostr client to see them here.</p> <p>Add bookmarks using your nostr client to see them here.</p>
<p>If you aren't on nostr yet, start here: <a href="https://nstart.me/" target="_blank" rel="noopener noreferrer">nstart.me</a></p>
</div> </div>
) : ( ) : (
<div className="bookmarks-list"> <div className="bookmarks-list">

View File

@@ -74,7 +74,7 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
followedPubkeys followedPubkeys
}) })
const { contentRef, handleMouseUp } = useHighlightInteractions({ const { contentRef, handleSelectionEnd } = useHighlightInteractions({
onHighlightClick, onHighlightClick,
selectedHighlightId, selectedHighlightId,
onTextSelection, onTextSelection,
@@ -138,7 +138,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
ref={contentRef} ref={contentRef}
className="reader-markdown" className="reader-markdown"
dangerouslySetInnerHTML={{ __html: finalHtml }} dangerouslySetInnerHTML={{ __html: finalHtml }}
onMouseUp={handleMouseUp} onMouseUp={handleSelectionEnd}
onTouchEnd={handleSelectionEnd}
/> />
) : ( ) : (
<div className="reader-markdown"> <div className="reader-markdown">
@@ -152,7 +153,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
ref={contentRef} ref={contentRef}
className="reader-html" className="reader-html"
dangerouslySetInnerHTML={{ __html: finalHtml || html || '' }} dangerouslySetInnerHTML={{ __html: finalHtml || html || '' }}
onMouseUp={handleMouseUp} onMouseUp={handleSelectionEnd}
onTouchEnd={handleSelectionEnd}
/> />
) )
) : ( ) : (

View File

@@ -28,36 +28,45 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
}) => { }) => {
const cachedImage = useImageCache(image, settings) const cachedImage = useImageCache(image, settings)
const formattedDate = published ? format(new Date(published * 1000), 'MMM d, yyyy') : null const formattedDate = published ? format(new Date(published * 1000), 'MMM d, yyyy') : null
const isLongSummary = summary && summary.length > 150
if (cachedImage) { if (cachedImage) {
return ( return (
<div className="reader-hero-image"> <>
<img src={cachedImage} alt={title || 'Article image'} /> <div className="reader-hero-image">
{formattedDate && ( <img src={cachedImage} alt={title || 'Article image'} />
<div className="publish-date-topright"> {formattedDate && (
{formattedDate} <div className="publish-date-topright">
</div> {formattedDate}
)}
{title && (
<div className="reader-header-overlay">
<h2 className="reader-title">{title}</h2>
{summary && <p className="reader-summary">{summary}</p>}
<div className="reader-meta">
{readingTimeText && (
<div className="reading-time">
<FontAwesomeIcon icon={faClock} />
<span>{readingTimeText}</span>
</div>
)}
{hasHighlights && (
<div className="highlight-indicator">
<FontAwesomeIcon icon={faHighlighter} />
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>
</div>
)}
</div> </div>
)}
{title && (
<div className="reader-header-overlay">
<h2 className="reader-title">{title}</h2>
{summary && <p className={`reader-summary ${isLongSummary ? 'hide-on-mobile' : ''}`}>{summary}</p>}
<div className="reader-meta">
{readingTimeText && (
<div className="reading-time">
<FontAwesomeIcon icon={faClock} />
<span>{readingTimeText}</span>
</div>
)}
{hasHighlights && (
<div className="highlight-indicator">
<FontAwesomeIcon icon={faHighlighter} />
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>
</div>
)}
</div>
</div>
)}
</div>
{isLongSummary && (
<div className="reader-summary-below-image">
<p className="reader-summary">{summary}</p>
</div> </div>
)} )}
</div> </>
) )
} }

View File

@@ -36,7 +36,7 @@ const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogou
accountManager.setActive(account) accountManager.setActive(account)
} catch (error) { } catch (error) {
console.error('Login failed:', error) console.error('Login failed:', error)
alert('Login failed. Please install a nostr browser extension and try again.') alert('Login failed. Please install a nostr browser extension and try again.\n\nIf you aren\'t on nostr yet, start here: https://nstart.me/')
} finally { } finally {
setIsConnecting(false) setIsConnecting(false)
} }

View File

@@ -56,8 +56,8 @@ export const useHighlightInteractions = ({
} }
}, [selectedHighlightId]) }, [selectedHighlightId])
// Handle text selection // Handle text selection (works for both mouse and touch)
const handleMouseUp = useCallback(() => { const handleSelectionEnd = useCallback(() => {
setTimeout(() => { setTimeout(() => {
const selection = window.getSelection() const selection = window.getSelection()
if (!selection || selection.rangeCount === 0) { if (!selection || selection.rangeCount === 0) {
@@ -76,6 +76,6 @@ export const useHighlightInteractions = ({
}, 10) }, 10)
}, [onTextSelection, onClearSelection]) }, [onTextSelection, onClearSelection])
return { contentRef, handleMouseUp } return { contentRef, handleSelectionEnd }
} }

View File

@@ -1437,6 +1437,51 @@ body.mobile-sidebar-open {
border: 1px solid rgba(100, 108, 255, 0.4); border: 1px solid rgba(100, 108, 255, 0.4);
} }
.reader-summary-below-image {
display: none;
}
@media (max-width: 768px) {
.reader-header-overlay .reader-summary.hide-on-mobile {
display: none;
}
.reader-summary-below-image {
display: block;
padding: 0 0 1.5rem 0;
margin-top: -1rem;
}
.reader-summary-below-image .reader-summary {
color: #aaa;
font-size: 1rem;
line-height: 1.6;
margin: 0;
}
.reader-hero-image {
min-height: 280px;
max-height: 400px;
height: 50vh;
}
.reader-hero-image img {
height: 100%;
width: 100%;
object-fit: cover;
object-position: center;
}
.reader-header-overlay {
padding: 1.5rem 1rem 1rem;
}
.reader-header-overlay .reader-title {
font-size: 1.5rem;
line-height: 1.3;
}
}
/* Private Bookmark Styles */ /* Private Bookmark Styles */
.private-bookmark { .private-bookmark {
background: #2a2a2a; background: #2a2a2a;
@@ -3062,4 +3107,13 @@ body.mobile-sidebar-open {
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: 1.5rem; gap: 1.5rem;
} }
.blog-post-card-summary {
-webkit-line-clamp: 2;
font-size: 0.8rem;
}
.blog-post-card-content {
padding: 1rem;
}
} }