1import { useEffect, useRef, useState } from 'react'; 2import { html } from '../html.js?v=3.8.36'; 3import { 4 AlertCircle, 5 Filter, 6 Folder, 7 LayoutGrid, 8 RotateCw, 9} from 'lucide-react'; 10import ArchiveResults from '../components/ArchiveResults.js?v=3.8.36'; 11import Sidebar from '../components/Sidebar.js?v=3.8.36'; 12import { RELEVANCE_SORT } from '../utils/searchRanking.js?v=3.8.36'; 13 14const DesktopArchivePanel = ({ 15 viewMode, 16 loading, 17 error, 18 filters, 19 setFilters, 20 facets, 21 autocompleteIndex, 22 sortBy, 23 setSortBy, 24 filteredRecords, 25 paginatedRecords, 26 folderGroups, 27 currentPage, 28 totalPages, 29 activeFilterCount, 30 onSetViewMode, 31 onSelectRecord, 32 onOpenFolder, 33 onPageChange, 34 onClearFilters, 35 onOpenStandard, 36}) => { 37 const [filterOpen, setFilterOpen] = useState(false); 38 const resultsRef = useRef(null); 39 const filterPanelRef = useRef(null); 40 const filterButtonRef = useRef(null); 41 42 useEffect(() => { 43 setFilterOpen(false); 44 }, [viewMode]); 45 46 useEffect(() => { 47 if (!filterOpen) return undefined; 48 let focusFrame = null; 49 let frame = null; 50 if (!filterPanelRef.current?.contains(document.activeElement)) { 51 frame = requestAnimationFrame(() => { 52 // The narrow drawer changes from visibility:hidden in the same commit. 53 // Give that style one painted frame before moving focus into it; a 54 // single requestAnimationFrame can run while the input is still treated 55 // as hidden and silently leave focus on the toggle. 56 focusFrame = requestAnimationFrame(() => { 57 document.getElementById('desktop-archive-search')?.focus(); 58 }); 59 }); 60 } 61 const handleKeyDown = (event) => { 62 const drawerActive = !window.matchMedia 63 || window.matchMedia('(max-width: 1100px)').matches; 64 if (event.key !== 'Escape' || !drawerActive) return; 65 event.preventDefault(); 66 setFilterOpen(false); 67 filterButtonRef.current?.focus(); 68 }; 69 document.addEventListener('keydown', handleKeyDown); 70 return () => { 71 if (frame !== null) cancelAnimationFrame(frame); 72 if (focusFrame !== null) cancelAnimationFrame(focusFrame); 73 document.removeEventListener('keydown', handleKeyDown); 74 }; 75 }, [filterOpen]); 76 77 useEffect(() => { 78 if (!window.matchMedia) return undefined; 79 const drawerQuery = window.matchMedia('(max-width: 1100px)'); 80 const preserveFocusedFilter = (event) => { 81 if (event.matches && filterPanelRef.current?.contains(document.activeElement)) { 82 setFilterOpen(true); 83 } 84 }; 85 drawerQuery.addEventListener('change', preserveFocusedFilter); 86 return () => drawerQuery.removeEventListener('change', preserveFocusedFilter); 87 }, []); 88 89 const closeFilters = () => { 90 setFilterOpen(false); 91 filterButtonRef.current?.focus(); 92 }; 93 94 const changePage = (page) => { 95 onPageChange(page); 96 const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches; 97 resultsRef.current?.scrollIntoView({ 98 behavior: reduceMotion ? 'auto' : 'smooth', 99 block: 'start', 100 }); 101 }; 102 103 const errorPanel = error && html` 104 <div className="desktop-archive-error archive-notice archive-notice--danger" role="alert"> 105 <${AlertCircle} aria-hidden="true" /> 106 <div> 107 <h3>Error loading archive</h3> 108 <p>${error}</p> 109 <button type="button" className="archive-action archive-action--danger" onClick=${() => window.location.reload()}> 110 <${RotateCw} aria-hidden="true" /> 111 Reload page 112 </button> 113 </div> 114 </div> 115 `; 116 117 return html` 118 <div className="desktop-archive-panel archive-density--compact"> 119 <header className="desktop-archive-heading"> 120 <div> 121 <p className="desktop-eyebrow">Canonical collection</p> 122 <h3>${viewMode === 'folder' ? 'Browse archive folders' : 'Search and read the archive'}</h3> 123 <p> 124 This window uses the archive's live records, filters, details, citations, and share links. 125 </p> 126 </div> 127 <button type="button" className="desktop-standard-link archive-action archive-action--secondary" onClick=${onOpenStandard}> 128 Open standard view 129 </button> 130 </header> 131 132 <div className="desktop-archive-toolbar" aria-label="Archive view controls"> 133 <div className="desktop-view-switch" role="group" aria-label="Archive layout"> 134 <button 135 type="button" 136 className=${viewMode === 'grid' ? 'is-active' : ''} 137 aria-pressed=${viewMode === 'grid'} 138 onClick=${() => onSetViewMode('grid')}
138 139 > 140 <${LayoutGrid} aria-hidden="true" /> 141 Cards 142 </button> 143 <button 144 type="button" 145 className=${viewMode === 'folder' ? 'is-active' : ''} 146 aria-pressed=${viewMode === 'folder'} 147 onClick=${() => onSetViewMode('folder')} 148 > 149 <${Folder} aria-hidden="true" /> 150 Folders 151 </button> 152 </div> 153 154 <button 155 ref=${filterButtonRef} 156 type="button" 157 className="desktop-filter-toggle" 158 aria-expanded=${filterOpen} 159 aria-controls="desktop-archive-filter-panel" 160 onClick=${() => setFilterOpen((open) => !open)} 161 > 162 <${Filter} aria-hidden="true" /> 163 Filters 164 ${activeFilterCount > 0 && html`<span>${activeFilterCount}</span>`} 165 </button> 166 167 <label className="desktop-sort-control"> 168 <span>Sort</span> 169 <select value=${sortBy} onChange=${(event) => setSortBy(event.target.value)}> 170 ${Boolean(filters.search.trim()) && html`<option value=${RELEVANCE_SORT}>Relevance</option>`} 171 <option value="date-desc">Newest first</option> 172 <option value="date-asc">Oldest first</option> 173 <option value="title-asc">Title (AâZ)</option> 174 </select> 175 </label> 176 </div> 177 178 <div className="desktop-archive-layout"> 179 <div id="desktop-archive-filter-panel" ref=${filterPanelRef}> 180 <${Sidebar} 181 facets=${facets} 182 filters=${filters} 183 setFilters=${setFilters} 184 isOpen=${filterOpen} 185 onClose=${closeFilters} 186 resetFilters=${onClearFilters} 187 autocompleteIndex=${autocompleteIndex} 188 variant="desktop" 189 /> 190 </div> 191 192 <section className="desktop-archive-result-column" aria-labelledby="desktop-result-summary" ref=${resultsRef}> 193 <p id="desktop-result-summary" className="desktop-result-summary" aria-live="polite"> 194 ${loading 195 ? 'Loading archiveâ¦' 196 : `${filteredRecords.length} record${filteredRecords.length === 1 ? '' : 's'} found${filters.recordIds !== null ? ' in query results' : ''}`} 197 </p> 198 <${ArchiveResults} 199 compact=${true} 200 errorPanel=${errorPanel} 201 loading=${loading} 202 filteredRecords=${filteredRecords} 203 paginatedRecords=${paginatedRecords} 204 folderGroups=${folderGroups} 205 viewMode=${viewMode} 206 searchTerm=${filters.search} 207 currentPage=${currentPage} 208 totalPages=${totalPages} 209 onSelectRecord=${onSelectRecord} 210 onOpenFolder=${onOpenFolder} 211 onPageChange=${changePage} 212 onClearFilters=${onClearFilters} 213 /> 214 </section> 215 </div> 216 </div> 217 `; 218}; 219 220export default DesktopArchivePanel;
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.