1 2import { useState, useEffect, useRef } from 'react'; 3import { html } from '../html.js?v=3.8.36'; 4import { X, FileText, Quote, BookOpen, Lightbulb, User } from 'lucide-react'; 5 6// Type labels for display 7const TYPE_LABELS = { 8 root: 'Dissertation', 9 intro: 'Introduction', 10 part: 'Part', 11 chapter: 'Chapter', 12 conclusion: 'Conclusion', 13 concept: 'Key concept', 14 figure: 'Key figure' 15}; 16 17const DetailPanel = ({ node, isOpen, onClose, contained = false }) => { 18 // Keep the last node data so panel can animate out with content 19 const [displayNode, setDisplayNode] = useState(node); 20 const panelRef = useRef(null); 21 const closeButtonRef = useRef(null); 22 const returnFocusRef = useRef(null); 23 const wasOpenRef = useRef(false); 24 25 useEffect(() => { 26 if (node) { 27 setDisplayNode(node); 28 } 29 // Don't clear displayNode when node becomes null - keep it for animation 30 }, [node]); 31 32 // Focus management: focus close button when panel opens 33 useEffect(() => { 34 if (isOpen) { 35 if (!wasOpenRef.current) returnFocusRef.current = document.activeElement; 36 wasOpenRef.current = true; 37 if (!displayNode) return undefined; 38 const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; 39 let frame = null; 40 let timer = null; 41 const focusClose = () => { 42 if (contained) panelRef.current?.scrollIntoView({ block: 'start' }); 43 closeButtonRef.current?.focus({ preventScroll: contained }); 44 }; 45 if (reduceMotion) frame = requestAnimationFrame(focusClose); 46 else timer = setTimeout(focusClose, 100); 47 return () => { 48 if (frame !== null) cancelAnimationFrame(frame); 49 if (timer !== null) clearTimeout(timer); 50 }; 51 } 52 53 if (wasOpenRef.current) { 54 wasOpenRef.current = false; 55 const trigger = returnFocusRef.current; 56 returnFocusRef.current = null; 57 requestAnimationFrame(() => trigger?.focus?.({ preventScroll: true })); 58 } 59 60 return undefined; 61 }, [isOpen, displayNode, contained]); 62 63 // Handle ESC key to close panel 64 useEffect(() => { 65 const handleKeyDown = (e) => { 66 if (e.key === 'Escape' && isOpen) { 67 onClose(); 68 } 69 }; 70 71 if (isOpen) { 72 document.addEventListener('keydown', handleKeyDown); 73 } 74 return () => document.removeEventListener('keydown', handleKeyDown); 75 }, [isOpen, onClose]); 76 77 // Don't render anything if we've never had a node 78 if (!displayNode) return null; 79 80 const positionClass = contained ? 'absolute' : 'fixed'; 81 82 return html` 83 ${isOpen && html` 84 <div 85 className=${`${positionClass} inset-0 bg-black/20 z-40 sm:hidden`} 86 style=${{ zIndex: 70 }} 87 onClick=${onClose} 88 /> 89 `} 90 91 <div 92 ref=${panelRef} 93 role="dialog" 94 aria-hidden=${isOpen ? undefined : 'true'} 95 inert=${isOpen ? undefined : ''} 96 aria-labelledby="detail-panel-title" 97 className=${` 98 archive-detail-panel archive-reading-panel ${contained ? 'is-contained' : ''} ${positionClass} top-0 h-full w-full sm:w-[420px] 99 shadow-xl z-50 transform transition-transform duration-300 ease-out 100 ${isOpen ? 'right-0' : '-right-full sm:-right-[420px]'} 101 `} 102 style=${{ 103 maxWidth: contained ? 'calc(100% - 16px)' : 'calc(100vw - 16px)', 104 zIndex: 80, 105 }} 106 > 107 <header className="archive-reading-panel__header"> 108 <span> 109 ${TYPE_LABELS[displayNode.type] || 'Section'} 110 </span> 111 <button 112 type="button" 113 ref=${closeButtonRef} 114 onClick=${onClose} 115 className="archive-reading-panel__close" 116 title="Close panel (ESC)" 117 aria-label="Close detail panel" 118 > 119 <${X} className="w-5 h-5 text-stone-500" /> 120 </button> 121 </header> 122 123 <div 124 className="archive-detail-content archive-reading-panel__document" 125 tabIndex="0" 126 aria-label="Detail panel content" 127 > 128 <h2 id="detail-panel-title"> 129 ${displayNode.label} 130 </h2> 131 132 ${displayNode.subtitle && html` 133 <p className="archive-reading-panel__subtitle">${displayNode.subtitle}</p> 134 `} 135 136 ${(displayNode.pageStart || displayNode.pageRef) && html` 137 <div className="archive-reading-panel__page-reference"> 138 <${FileText} className="w-3.5 h-3.5" />
139 <span> 140 ${displayNode.pageRef || `Pages ${displayNode.pageStart}${displayNode.pageEnd ? `â${displayNode.pageEnd}` : '+'}`} 141 </span> 142 </div> 143 `} 144 145 ${displayNode.pullQuote && html` 146 <section className="archive-reading-panel__section"> 147 <h3> 148 <${Quote} className="w-3.5 h-3.5" /> 149 <span>From the text</span> 150 </h3> 151 <blockquote> 152 â${displayNode.pullQuote}â 153 </blockquote> 154 </section> 155 `} 156 157 ${displayNode.summary && html` 158 <section className="archive-reading-panel__section"> 159 <h3> 160 <${BookOpen} className="w-3.5 h-3.5" /> 161 <span>Summary</span> 162 </h3> 163 <p> 164 ${displayNode.summary} 165 </p> 166 </section> 167 `} 168 169 ${displayNode.keyConcepts && displayNode.keyConcepts.length > 0 && html` 170 <section className="archive-reading-panel__section"> 171 <h3> 172 <${Lightbulb} className="w-3.5 h-3.5" /> 173 <span>Key concepts</span> 174 </h3> 175 <div className="archive-reading-panel__tags"> 176 ${displayNode.keyConcepts.map((concept, i) => html` 177 <span 178 key=${i} 179 className="archive-reading-panel__tag" 180 > 181 ${concept} 182 </span> 183 `)} 184 </div> 185 </section> 186 `} 187 188 ${displayNode.keyFigures && displayNode.keyFigures.length > 0 && html` 189 <section className="archive-reading-panel__section"> 190 <h3> 191 <${User} className="w-3.5 h-3.5" /> 192 <span>Key figures</span> 193 </h3> 194 <div className="archive-reading-panel__tags"> 195 ${displayNode.keyFigures.map((figure, i) => html` 196 <span 197 key=${i} 198 className="archive-reading-panel__tag" 199 > 200 ${figure} 201 </span> 202 `)} 203 </div> 204 </section> 205 `} 206 207 ${!displayNode.summary && !displayNode.pullQuote && !displayNode.keyConcepts?.length && !displayNode.keyFigures?.length && html` 208 <div className="archive-reading-panel__empty"> 209 <${FileText} className="w-8 h-8 mx-auto mb-2 opacity-30" /> 210 <p className="text-sm">No additional detail for this item</p> 211 </div> 212 `} 213 </div> 214 </div> 215 `; 216}; 217 218export default DetailPanel;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.