mirror of
https://github.com/dergigi/boris.git
synced 2025-12-18 15:14:20 +01:00
feat: add mobile highlights panel as overlay with toggle button
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faBookmark } from '@fortawesome/free-solid-svg-icons'
|
import { faBookmark, faHighlighter } from '@fortawesome/free-solid-svg-icons'
|
||||||
import { RelayPool } from 'applesauce-relay'
|
import { RelayPool } from 'applesauce-relay'
|
||||||
import { IEventStore } from 'applesauce-core'
|
import { IEventStore } from 'applesauce-core'
|
||||||
import { BookmarkList } from './BookmarkList'
|
import { BookmarkList } from './BookmarkList'
|
||||||
@@ -85,10 +85,11 @@ interface ThreePaneLayoutProps {
|
|||||||
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null)
|
const sidebarRef = useRef<HTMLDivElement>(null)
|
||||||
|
const highlightsRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
// Lock body scroll when mobile sidebar is open
|
// Lock body scroll when mobile sidebar or highlights is open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile && props.isSidebarOpen) {
|
if (isMobile && (props.isSidebarOpen || !props.isHighlightsCollapsed)) {
|
||||||
document.body.classList.add('mobile-sidebar-open')
|
document.body.classList.add('mobile-sidebar-open')
|
||||||
} else {
|
} else {
|
||||||
document.body.classList.remove('mobile-sidebar-open')
|
document.body.classList.remove('mobile-sidebar-open')
|
||||||
@@ -97,21 +98,26 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
return () => {
|
return () => {
|
||||||
document.body.classList.remove('mobile-sidebar-open')
|
document.body.classList.remove('mobile-sidebar-open')
|
||||||
}
|
}
|
||||||
}, [isMobile, props.isSidebarOpen])
|
}, [isMobile, props.isSidebarOpen, props.isHighlightsCollapsed])
|
||||||
|
|
||||||
// Handle ESC key to close sidebar
|
// Handle ESC key to close sidebar or highlights
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMobile || !props.isSidebarOpen) return
|
if (!isMobile) return
|
||||||
|
if (!props.isSidebarOpen && props.isHighlightsCollapsed) return
|
||||||
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
props.onToggleSidebar()
|
if (props.isSidebarOpen) {
|
||||||
|
props.onToggleSidebar()
|
||||||
|
} else if (!props.isHighlightsCollapsed) {
|
||||||
|
props.onToggleHighlightsPanel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', handleEscape)
|
document.addEventListener('keydown', handleEscape)
|
||||||
return () => document.removeEventListener('keydown', handleEscape)
|
return () => document.removeEventListener('keydown', handleEscape)
|
||||||
}, [isMobile, props.isSidebarOpen, props.onToggleSidebar])
|
}, [isMobile, props.isSidebarOpen, props.isHighlightsCollapsed, props.onToggleSidebar, props.onToggleHighlightsPanel])
|
||||||
|
|
||||||
// Trap focus in sidebar when open on mobile
|
// Trap focus in sidebar when open on mobile
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -148,16 +154,55 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}, [isMobile, props.isSidebarOpen])
|
}, [isMobile, props.isSidebarOpen])
|
||||||
|
|
||||||
|
// Trap focus in highlights panel when open on mobile
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMobile || props.isHighlightsCollapsed || !highlightsRef.current) return
|
||||||
|
|
||||||
|
const highlights = highlightsRef.current
|
||||||
|
const focusableElements = highlights.querySelectorAll<HTMLElement>(
|
||||||
|
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||||
|
)
|
||||||
|
const firstElement = focusableElements[0]
|
||||||
|
const lastElement = focusableElements[focusableElements.length - 1]
|
||||||
|
|
||||||
|
const handleTab = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Tab') return
|
||||||
|
|
||||||
|
if (e.shiftKey) {
|
||||||
|
if (document.activeElement === firstElement) {
|
||||||
|
e.preventDefault()
|
||||||
|
lastElement?.focus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (document.activeElement === lastElement) {
|
||||||
|
e.preventDefault()
|
||||||
|
firstElement?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
highlights.addEventListener('keydown', handleTab)
|
||||||
|
firstElement?.focus()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
highlights.removeEventListener('keydown', handleTab)
|
||||||
|
}
|
||||||
|
}, [isMobile, props.isHighlightsCollapsed])
|
||||||
|
|
||||||
const handleBackdropClick = () => {
|
const handleBackdropClick = () => {
|
||||||
if (isMobile && props.isSidebarOpen) {
|
if (isMobile) {
|
||||||
props.onToggleSidebar()
|
if (props.isSidebarOpen) {
|
||||||
|
props.onToggleSidebar()
|
||||||
|
} else if (!props.isHighlightsCollapsed) {
|
||||||
|
props.onToggleHighlightsPanel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Mobile bookmark button */}
|
{/* Mobile bookmark button */}
|
||||||
{isMobile && !props.isSidebarOpen && (
|
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && (
|
||||||
<button
|
<button
|
||||||
className="mobile-hamburger-btn"
|
className="mobile-hamburger-btn"
|
||||||
onClick={props.onToggleSidebar}
|
onClick={props.onToggleSidebar}
|
||||||
@@ -168,10 +213,22 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Mobile highlights button */}
|
||||||
|
{isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && (
|
||||||
|
<button
|
||||||
|
className="mobile-highlights-btn"
|
||||||
|
onClick={props.onToggleHighlightsPanel}
|
||||||
|
aria-label="Open highlights"
|
||||||
|
aria-expanded={!props.isHighlightsCollapsed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHighlighter} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Mobile backdrop */}
|
{/* Mobile backdrop */}
|
||||||
{isMobile && (
|
{isMobile && (
|
||||||
<div
|
<div
|
||||||
className={`mobile-sidebar-backdrop ${props.isSidebarOpen ? 'visible' : ''}`}
|
className={`mobile-sidebar-backdrop ${(props.isSidebarOpen || !props.isHighlightsCollapsed) ? 'visible' : ''}`}
|
||||||
onClick={handleBackdropClick}
|
onClick={handleBackdropClick}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@@ -240,7 +297,11 @@ const ThreePaneLayout: React.FC<ThreePaneLayoutProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="pane highlights">
|
<div
|
||||||
|
ref={highlightsRef}
|
||||||
|
className={`pane highlights ${isMobile && !props.isHighlightsCollapsed ? 'mobile-open' : ''}`}
|
||||||
|
aria-hidden={isMobile && props.isHighlightsCollapsed}
|
||||||
|
>
|
||||||
<HighlightsPanel
|
<HighlightsPanel
|
||||||
highlights={props.highlights}
|
highlights={props.highlights}
|
||||||
loading={props.highlightsLoading}
|
loading={props.highlightsLoading}
|
||||||
|
|||||||
@@ -622,7 +622,24 @@ body.mobile-sidebar-open {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pane.highlights {
|
.pane.highlights {
|
||||||
display: none;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 85%;
|
||||||
|
max-width: 320px;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
background: #1a1a1a;
|
||||||
|
z-index: 1000;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane.highlights.mobile-open {
|
||||||
|
transform: translateX(0);
|
||||||
|
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-sidebar-backdrop {
|
.mobile-sidebar-backdrop {
|
||||||
@@ -642,6 +659,35 @@ body.mobile-sidebar-open {
|
|||||||
display: block;
|
display: block;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mobile-highlights-btn {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 900;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #ddd;
|
||||||
|
width: var(--min-touch-target);
|
||||||
|
height: var(--min-touch-target);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-highlights-btn:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-highlights-btn {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.reader {
|
.reader {
|
||||||
|
|||||||
Reference in New Issue
Block a user