Compare commits

...

10 Commits

Author SHA1 Message Date
Gigi
66b060627a chore: bump version to 0.5.1 2025-10-12 23:54:17 +02:00
Gigi
d9bcf14baa fix: match highlight count indicator style to reading-time element
- Use rgba() with 0.1 opacity for background (subtle, not bright)
- Use rgba() with 0.3 opacity for border
- Set text and icon color to white for better visibility
- Properly converts hex colors to rgba using hexToRgb helper
2025-10-12 23:53:10 +02:00
Gigi
c571e6ebf7 fix: reduce brightness and add colored border to highlight count indicator
- Set background color to 75% opacity (bf hex) to reduce brightness
- Add border color matching the highlight group color
- Makes the indicator less overwhelming while maintaining visibility
2025-10-12 23:51:05 +02:00
Gigi
fb06a1aec3 fix: apply user highlight color to both marker and arrow icons
- Apply color style directly to both icons in collapsed highlights button
- Update CSS to use color-agnostic pulse animation (opacity/scale)
- Remove hardcoded yellow color and drop-shadow from glow effect
2025-10-12 23:50:34 +02:00
Gigi
5a0d08641b chore: remove MOBILE_IMPLEMENTATION.md
No longer needed as mobile features are now integrated
2025-10-12 23:49:12 +02:00
Gigi
8a8419385e fix: apply highlight group color to background of count indicator
- Change highlight count indicator to use backgroundColor instead of color
- Set text color to black for contrast against colored backgrounds
- Maintains priority: nostrverse > friends > mine
2025-10-12 23:48:56 +02:00
Gigi
0d5dc6e785 feat: apply 'my highlights' color to collapsed highlights button
- Update HighlightsPanelCollapsed to accept settings prop
- Apply highlightColorMine to the expand button icon and text
- Pass settings through HighlightsPanel and ThreePaneLayout
2025-10-12 23:48:18 +02:00
Gigi
1d90333803 feat: apply highlight group color to highlight count indicator
- Update ReaderHeader to receive highlights and highlightVisibility props
- Calculate dominant color based on visible highlight groups
- Apply color priority: nostrverse > friends > mine
- Highlight count indicator now reflects the active group colors
2025-10-12 23:47:05 +02:00
Gigi
91e6e62688 feat: apply 'my highlights' color to highlight buttons
- Update floating highlight button (FAB) to use highlightColorMine from settings
- Update mobile highlights button to use highlightColorMine from settings
- Both buttons now reflect the user's chosen 'my highlights' color
2025-10-12 23:44:51 +02:00
Gigi
619a8a9753 docs(changelog): add v0.5.0 changes and compare links 2025-10-12 23:42:44 +02:00
9 changed files with 115 additions and 170 deletions

View File

@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.5.0] - 2025-10-12
### Added
- Upgrade to full PWA with `vite-plugin-pwa`
- Replace placeholder icons with branded favicons
- Author info card for nostr-native articles
### Changed
- Explore: shrink refresh spinner footprint; inline-sized loading row
- Explore: preserve posts across navigations; seed from cache; merge streamed and final results
- Explore: keep posts visible during refresh; inline spinner; no list wipe
- Bookmarks: keep list visible during refresh; show spinner only; no wipe
- Bookmarks: avoid clearing list when no new events; decouple refetch from route changes
- Highlights: split service into smaller modules to keep files under 210 lines
- Lint/TypeScript: satisfy react-hooks dependencies; fix worker typings; clear ESLint/TS issues
### Fixed
- Highlights: merge remote results after local for article/url
- Explore: always query remote relays after local; stream merge into UI
- Improve mobile touch targets for highlight icons
- Color `/me` highlights with "my highlights" color setting
### Performance
- Local-first then remote follow-up across services (titles, bookmarks, highlights)
- Run local and remote fetches concurrently; stream and dedupe results
- Stream contacts and early posts from local; merge remote later
- Relay queries use local-first with short timeouts; fallback to remote when needed
- Stream results to UI; display cached/local immediately (articles, highlights, explore)
### Documentation
- PWA implementation summary and launch checklist updates
- Update docs to reflect branded icons and final steps
- Remove temporary PWA launch checklist and implementation summary
## [0.4.3] - 2025-10-11
### Added
@@ -658,6 +692,8 @@ 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.5.0...HEAD
[0.5.0]: https://github.com/dergigi/boris/compare/v0.4.3...v0.5.0
[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

View File

@@ -1,156 +0,0 @@
# Mobile Implementation Summary
## Overview
Boris is now mobile-friendly! The app now works seamlessly on mobile devices with a responsive design that includes:
- Auto-collapsing sidebar that opens as an overlay drawer on small screens
- Touch-optimized UI with proper touch target sizes (44x44px minimum)
- Safe area insets for notched devices (iPhone X+, etc.)
- Focus trap and keyboard navigation in the mobile sidebar
- Mobile-optimized modals, toasts, and other UI elements
## Changes Made
### 1. Viewport & Base Setup
**File: `index.html`**
- Updated viewport meta tag to include `viewport-fit=cover` for proper safe area handling
### 2. Media Query Hooks
**File: `src/hooks/useMediaQuery.ts` (NEW)**
- `useMediaQuery(query)` - Generic hook for any media query
- `useIsMobile()` - Detects mobile viewport (≤768px)
- `useIsTablet()` - Detects tablet viewport (≤1024px)
- `useIsCoarsePointer()` - Detects touch devices
### 3. Mobile CSS Styles
**File: `src/index.css`**
- Added CSS custom properties for mobile breakpoints and safe areas
- Mobile-specific three-pane layout that stacks into single column
- Overlay sidebar with backdrop and transitions
- Touch target improvements (44x44px minimum)
- Disabled hover effects on touch devices
- Mobile-optimized modals (full-screen sheet style)
- Mobile-optimized toasts (bottom position with safe area)
- Dynamic viewport height support (`100dvh`)
- Overscroll behavior and body scroll locking
### 4. Sidebar State Management
**File: `src/hooks/useBookmarksUI.ts`**
- Added `isMobile` state from media query
- Added `isSidebarOpen` state for mobile overlay
- Added `toggleSidebar()` function
- Auto-collapse logic based on `autoCollapseSidebarOnMobile` setting
- Mobile sidebar defaults to closed, desktop defaults to open
### 5. Three-Pane Layout Mobile Support
**File: `src/components/ThreePaneLayout.tsx`**
- Mobile hamburger button (visible only on mobile)
- Mobile backdrop for closing sidebar
- Body scroll locking when sidebar is open
- ESC key handler to close sidebar
- Focus trap in sidebar (Tab navigation stays within sidebar)
- Focus restoration when closing sidebar
- Accessibility attributes (`aria-hidden`, `aria-expanded`, etc.)
### 6. Sidebar Header Mobile Controls
**File: `src/components/SidebarHeader.tsx`**
- Close button (X) visible on mobile instead of collapse chevron
- Hamburger button hidden in header (shown in layout instead)
### 7. Bookmark List Mobile Props
**File: `src/components/BookmarkList.tsx`**
- Added `isMobile` prop support
- Passes mobile state to SidebarHeader
### 8. Main Bookmarks Component
**File: `src/components/Bookmarks.tsx`**
- Uses mobile state from `useBookmarksUI`
- Auto-closes sidebar when selecting bookmark on mobile
- Closes sidebar when opening settings on mobile
- Proper desktop/mobile toggle behavior
### 9. Icon Button Enhancement
**File: `src/components/IconButton.tsx`**
- Added optional `className` prop for additional styling
### 10. Mobile Settings
**File: `src/services/settingsService.ts`**
- Added `autoCollapseSidebarOnMobile?: boolean` setting (default: true)
**File: `src/components/Settings/StartupPreferencesSettings.tsx`**
- Added UI toggle for "Auto-collapse sidebar on small screens"
## Accessibility Features
- Focus trap in mobile sidebar (Tab key navigation stays within drawer)
- ESC key closes mobile sidebar
- Backdrop click closes mobile sidebar
- Proper ARIA attributes (`aria-hidden`, `aria-expanded`, `aria-controls`)
- Touch target minimum size enforcement (44x44px)
- Focus restoration when closing sidebar
## Mobile Behaviors
1. **Sidebar**: Slides in from left as overlay drawer with backdrop
2. **Hamburger Menu**: Fixed position top-left when sidebar closed
3. **Selecting Content**: Auto-closes sidebar on mobile
4. **Opening Settings**: Auto-closes sidebar on mobile
5. **Highlights Panel**: Hidden on mobile (content takes full width)
6. **Modals**: Full-screen sheet style from bottom
7. **Toasts**: Bottom position with safe area padding
## Responsive Breakpoints
- **Mobile**: ≤768px (sidebar overlay, single column)
- **Tablet**: ≤1024px (defined but not actively used yet)
- **Desktop**: >768px (three-pane layout as before)
## Browser Support
- Modern browsers with CSS Grid support
- iOS Safari (including safe area insets)
- Chrome for Android
- Firefox Mobile
- Safari on iPadOS
## Safe Area Support
The app respects device safe areas (notches, home indicators) through CSS environment variables:
- `env(safe-area-inset-top)`
- `env(safe-area-inset-bottom)`
- `env(safe-area-inset-left)`
- `env(safe-area-inset-right)`
## Future Enhancements
Potential improvements for future iterations:
- Swipe gesture to open/close sidebar
- Pull-to-refresh on mobile
- Bottom sheet for highlights panel on mobile
- Optimized font sizes for mobile reading
- Mobile-specific view mode (perhaps auto-switch to compact on mobile)
- Haptic feedback on interactions (iOS/Android)
- Share sheet integration
- Install prompt for PWA
## Testing Checklist
- [x] Sidebar opens/closes on mobile
- [x] Hamburger button visible on mobile
- [x] Backdrop closes sidebar
- [x] ESC key closes sidebar
- [x] Focus trap works in sidebar
- [x] Selecting bookmark closes sidebar
- [x] No horizontal scroll
- [x] Touch targets ≥ 44px
- [x] Modals are full-screen on mobile
- [x] Toasts appear at bottom with safe area
- [x] Build completes without errors
- [ ] Test on actual iOS device (iPhone)
- [ ] Test on actual Android device
- [ ] Test with keyboard navigation
- [ ] Test with screen reader
- [ ] Test landscape orientation
- [ ] Test on various screen sizes (320px, 375px, 414px, 768px)
## Commit History
1. `feat: update viewport meta for mobile support`
2. `feat: add media query hooks for responsive design`
3. `feat: add mobile sidebar state management to useBookmarksUI`
4. `feat: add mobile-responsive CSS with breakpoints and safe areas`
5. `feat: implement mobile overlay sidebar with focus trap and ESC handling`
6. `feat: add mobile auto-collapse setting`
7. `fix: resolve TypeScript errors for mobile implementation`

View File

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

View File

@@ -180,6 +180,8 @@ const ContentPanel: React.FC<ContentPanelProps> = ({
hasHighlights={hasHighlights}
highlightCount={relevantHighlights.length}
settings={settings}
highlights={relevantHighlights}
highlightVisibility={highlightVisibility}
/>
{markdown || html ? (
<>

View File

@@ -8,6 +8,7 @@ import HighlightsPanelCollapsed from './HighlightsPanel/HighlightsPanelCollapsed
import HighlightsPanelHeader from './HighlightsPanel/HighlightsPanelHeader'
import { RelayPool } from 'applesauce-relay'
import { IEventStore } from 'applesauce-core'
import { UserSettings } from '../services/settingsService'
export interface HighlightVisibility {
nostrverse: boolean
@@ -32,6 +33,7 @@ interface HighlightsPanelProps {
followedPubkeys?: Set<string>
relayPool?: RelayPool | null
eventStore?: IEventStore | null
settings?: UserSettings
}
export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
@@ -50,7 +52,8 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
onHighlightVisibilityChange,
followedPubkeys = new Set(),
relayPool,
eventStore
eventStore,
settings
}) => {
const [showHighlights, setShowHighlights] = useState(true)
const [localHighlights, setLocalHighlights] = useState(highlights)
@@ -90,6 +93,7 @@ export const HighlightsPanel: React.FC<HighlightsPanelProps> = ({
<HighlightsPanelCollapsed
hasHighlights={filteredHighlights.length > 0}
onToggleCollapse={onToggleCollapse}
settings={settings}
/>
)
}

View File

@@ -1,16 +1,21 @@
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHighlighter, faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { UserSettings } from '../../services/settingsService'
interface HighlightsPanelCollapsedProps {
hasHighlights: boolean
onToggleCollapse: () => void
settings?: UserSettings
}
const HighlightsPanelCollapsed: React.FC<HighlightsPanelCollapsedProps> = ({
hasHighlights,
onToggleCollapse
onToggleCollapse,
settings
}) => {
const highlightColor = settings?.highlightColorMine || '#ffff00'
return (
<div className="highlights-container collapsed">
<button
@@ -19,8 +24,12 @@ const HighlightsPanelCollapsed: React.FC<HighlightsPanelCollapsedProps> = ({
title="Expand highlights panel"
aria-label="Expand highlights panel"
>
<FontAwesomeIcon icon={faHighlighter} className={hasHighlights ? 'glow' : ''} />
<FontAwesomeIcon icon={faChevronRight} />
<FontAwesomeIcon
icon={faHighlighter}
className={hasHighlights ? 'glow' : ''}
style={{ color: highlightColor }}
/>
<FontAwesomeIcon icon={faChevronRight} style={{ color: highlightColor }} />
</button>
</div>
)

View File

@@ -1,9 +1,12 @@
import React from 'react'
import React, { useMemo } from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHighlighter, faClock } from '@fortawesome/free-solid-svg-icons'
import { format } from 'date-fns'
import { useImageCache } from '../hooks/useImageCache'
import { UserSettings } from '../services/settingsService'
import { Highlight, HighlightLevel } from '../types/highlights'
import { HighlightVisibility } from './HighlightsPanel'
import { hexToRgb } from '../utils/colorHelpers'
interface ReaderHeaderProps {
title?: string
@@ -14,6 +17,8 @@ interface ReaderHeaderProps {
hasHighlights: boolean
highlightCount: number
settings?: UserSettings
highlights?: Highlight[]
highlightVisibility?: HighlightVisibility
}
const ReaderHeader: React.FC<ReaderHeaderProps> = ({
@@ -24,12 +29,46 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
readingTimeText,
hasHighlights,
highlightCount,
settings
settings,
highlights = [],
highlightVisibility = { nostrverse: true, friends: true, mine: true }
}) => {
const cachedImage = useImageCache(image, settings)
const formattedDate = published ? format(new Date(published * 1000), 'MMM d, yyyy') : null
const isLongSummary = summary && summary.length > 150
// Determine the dominant highlight color based on visibility and priority
const highlightIndicatorStyles = useMemo(() => {
if (!highlights.length) return undefined
// Count highlights by level that are visible
const visibleLevels = new Set<HighlightLevel>()
highlights.forEach(h => {
if (h.level && highlightVisibility[h.level]) {
visibleLevels.add(h.level)
}
})
let hexColor: string | undefined
// Priority: nostrverse > friends > mine
if (visibleLevels.has('nostrverse') && highlightVisibility.nostrverse) {
hexColor = settings?.highlightColorNostrverse || '#9333ea'
} else if (visibleLevels.has('friends') && highlightVisibility.friends) {
hexColor = settings?.highlightColorFriends || '#f97316'
} else if (visibleLevels.has('mine') && highlightVisibility.mine) {
hexColor = settings?.highlightColorMine || '#ffff00'
}
if (!hexColor) return undefined
const rgb = hexToRgb(hexColor)
return {
backgroundColor: `rgba(${rgb}, 0.1)`,
borderColor: `rgba(${rgb}, 0.3)`,
color: '#fff'
}
}, [highlights, highlightVisibility, settings])
if (cachedImage) {
return (
<>
@@ -52,7 +91,10 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
</div>
)}
{hasHighlights && (
<div className="highlight-indicator">
<div
className="highlight-indicator"
style={highlightIndicatorStyles}
>
<FontAwesomeIcon icon={faHighlighter} />
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>
</div>
@@ -89,7 +131,10 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
</div>
)}
{hasHighlights && (
<div className="highlight-indicator">
<div
className="highlight-indicator"
style={highlightIndicatorStyles}
>
<FontAwesomeIcon icon={faHighlighter} />
<span>{highlightCount} highlight{highlightCount !== 1 ? 's' : ''}</span>
</div>

View File

@@ -241,6 +241,10 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
onClick={props.onToggleHighlightsPanel}
aria-label="Open highlights"
aria-expanded={!props.isHighlightsCollapsed}
style={{
backgroundColor: props.settings.highlightColorMine || '#ffff00',
color: '#000'
}}
>
<FontAwesomeIcon icon={faHighlighter} />
</button>
@@ -351,6 +355,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
followedPubkeys={props.followedPubkeys}
relayPool={props.relayPool}
eventStore={props.eventStore}
settings={props.settings}
/>
</div>
</div>
@@ -358,7 +363,7 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
<HighlightButton
ref={props.highlightButtonRef}
onHighlight={props.onCreateHighlight}
highlightColor={props.settings.highlightColor || '#ffff00'}
highlightColor={props.settings.highlightColorMine || '#ffff00'}
/>
)}
<RelayStatusIndicator relayPool={props.relayPool} />

View File

@@ -1836,17 +1836,17 @@ body.mobile-sidebar-open {
}
.highlights-container.collapsed .toggle-highlights-btn .glow {
color: #ffff00;
filter: drop-shadow(0 0 4px rgba(255, 255, 0, 0.6));
animation: pulse-glow 2s ease-in-out infinite;
}
@keyframes pulse-glow {
0%, 100% {
filter: drop-shadow(0 0 4px rgba(255, 255, 0, 0.6));
opacity: 0.8;
transform: scale(1);
}
50% {
filter: drop-shadow(0 0 8px rgba(255, 255, 0, 0.9));
opacity: 1;
transform: scale(1.1);
}
}