1import { html } from '../html.js?v=3.8.36'; 2import { ChevronLeft, ChevronRight, FolderOpen, SearchX } from 'lucide-react'; 3import { COLORS } from '../constants.js?v=3.8.36'; 4import { hashString } from '../services/archiveService.js?v=3.8.36'; 5import { recordNeedsReview } from '../utils/needsReview.js?v=3.8.36'; 6import { LEXICAL_SIGNAL, presentSearchSignal } from '../utils/searchRanking.js?v=3.8.36'; 7import { canonicalRecordUrl } from '../utils/recordDeepLink.js?v=3.8.36'; 8import LoadingQuotes from './LoadingQuotes.js?v=3.8.36'; 9 10const Highlight = ({ text, term }) => { 11 if (!term || term.length < 2) return html`<span>${text}</span>`; 12 const parts = text.split(new RegExp(`(${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')); 13 return html` 14 <span> 15 ${parts.map((part, index) => ( 16 part.toLowerCase() === term.toLowerCase() 17 ? html`<mark key=${index}>${part}</mark>` 18 : part 19 ))} 20 </span> 21 `; 22}; 23 24/** 25 * The canonical archive-result renderer shared by the standard archive and 26 * the optional desktop adapter. Filtering, sorting, pagination, and record 27 * selection remain owned by App; this component only presents those results. 28 */ 29const ArchiveResults = ({ 30 compact = false, 31 errorPanel = null, 32 loading, 33 filteredRecords, 34 paginatedRecords, 35 folderGroups, 36 viewMode, 37 searchTerm, 38 // Map of record id to its hybrid-search provenance chip: 'kw', 'sem', or 39 // 'kw·sem' (#279). Null unless semantic search is on, so a plain keyword 40 // search does not label every card. A record the ranked legs never reached 41 // still matched the typed words by substring, so it gets the keyword chip: 42 // while chips are on, every card carries one. 43 searchSignals = null, 44 currentPage, 45 totalPages, 46 onSelectRecord, 47 onOpenFolder, 48 onPageChange, 49 onClearFilters, 50 // Brief stuck-record skip for the broken-record extra (#754). Off by 51 // default, and the CSS drops the movement under reduced motion. 52 skipping = false, 53}) => { 54 const openCard = (event, recordId) => { 55 if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return; 56 event.preventDefault(); 57 onSelectRecord(recordId); 58 }; 59 60 return html` 61 <div className=${`${compact ? 'archive-results archive-results--compact desktop-archive-results' : 'archive-results'}${skipping ? ' archive-results--skipping' : ''}`}> 62 ${errorPanel} 63 64 ${!errorPanel && loading && html`<${LoadingQuotes} />`} 65 66 ${!errorPanel && !loading && filteredRecords.length === 0 && html` 67 <div className="archive-empty-state"> 68 <${SearchX} className="archive-empty-state__icon" aria-hidden="true" /> 69 <p className="archive-section-label">Search result</p> 70 <h3>No records found</h3> 71 <p>Try adjusting your search terms or filters.</p> 72 <button 73 type="button" 74 onClick=${onClearFilters} 75 className="archive-action archive-action--secondary" 76 > 77 Clear all filters 78 </button> 79 </div> 80 `} 81 82 ${!errorPanel && !loading && viewMode === 'grid' && html` 83 <div className=${compact 84 ? 'desktop-archive-card-grid' 85 : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6'}> 86 ${paginatedRecords.map((item) => { 87 const primaryCategory = item.categories[0] || 'Uncategorized'; 88 const colorIndex = hashString(primaryCategory) % COLORS.length; 89 const theme = COLORS[colorIndex]; 90 const signal = searchSignals 91 ? (searchSignals.get(item.id) || LEXICAL_SIGNAL) 92 : null; 93 const signalCopy = signal ? presentSearchSignal(signal) : null; 94 95 return html` 96 <article 97 key=${item.id} 98 className="archive-record-card" 99 > 100 <span 101 className="archive-record-card__accent" 102 style=${{ backgroundColor: theme.text }} 103 aria-hidden="true" 104 /> 105 <a 106 href=${canonicalRecordUrl(window.location.href, item.id)} 107 aria-label=${`Open record: ${item.title}`} 108 onClick=${event => openCard(event, item.id)} 109 className="archive-record-card__body" 110 > 111 <div className="archive-record-card__meta">
112 <span className="archive-record-card__publication">${item.pub}</span> 113 <span className="archive-record-card__date">${item.date}</span> 114 </div> 115 116 <h3 className="archive-record-card__title"> 117 <${Highlight} text=${item.title} term=${searchTerm} /> 118 </h3> 119 120 <p className="archive-record-card__summary"> 121 <${Highlight} text=${item.summaryPreview || item.summary || ''} term=${searchTerm} /> 122 </p> 123 124 <div className="archive-record-card__labels"> 125 ${signal && html` 126 <span 127 className="archive-record-card__label archive-record-card__label--signal" 128 title=${signalCopy.description} 129 > 130 ${signalCopy.label} 131 </span> 132 `} 133 ${item.categories.slice(0, 2).map((category, index) => html` 134 <span 135 key=${category} 136 className="archive-record-card__label" 137 style=${index === 0 ? { 138 backgroundColor: theme.bg, 139 borderColor: theme.border, 140 color: theme.text, 141 } : undefined} 142 > 143 ${category} 144 </span> 145 `)} 146 ${item.categories.length > 2 && html` 147 <span className="archive-record-card__label"> 148 +${item.categories.length - 2} more 149 </span> 150 `} 151 ${recordNeedsReview(item) && html` 152 <span 153 className="archive-record-card__label archive-record-card__label--review" 154 style=${{ backgroundColor: '#fffbeb', color: '#b45309' }} 155 title="Auto-submitted; pending a human review pass" 156 > 157 needs review 158 </span> 159 `} 160 </div> 161 </a> 162 </article> 163 `; 164 })} 165 </div> 166 `} 167 168 ${!errorPanel && !loading && viewMode === 'folder' && html` 169 <div className=${compact 170 ? 'desktop-archive-folder-grid' 171 : 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6'}> 172 ${folderGroups.map((group) => { 173 const colorIndex = hashString(group.name) % COLORS.length; 174 const theme = COLORS[colorIndex]; 175 176 return html` 177 <button 178 type="button" 179 key=${group.name} 180 onClick=${() => onOpenFolder(group.name)} 181 className="archive-folder-card" 182 > 183 <span 184 className="archive-folder-tab archive-folder-card__tab" 185 style=${{ 186 '--archive-folder-tab-fill': theme.bg, 187 '--archive-folder-tab-edge': theme.border, 188 }} 189 > 190 <span 191 className="archive-folder-card__count" 192 style=${{ color: theme.text }} 193 > 194 ${group.count} records 195 </span> 196 </span> 197 <span className="archive-folder-card__body"> 198 <span className="archive-folder-card__accent" style=${{ backgroundColor: theme.text }}></span> 199 <span className="archive-folder-card__name"> 200 ${group.name} 201 </span> 202 <${FolderOpen} className="archive-folder-card__icon" aria-hidden="true" /> 203 </span> 204 </button> 205 `; 206 })} 207 </div> 208 `} 209 210 ${!loading && !errorPanel && viewMode === 'grid' && totalPages > 1 && html` 211 <nav className="archive-pagination" aria-label="Archive results pages"> 212 <button 213 type="button" 214 onClick=${() => onPageChange(currentPage - 1)} 215 disabled=${currentPage === 1} 216 className="archive-action archive-action--quiet archive-pagination__button" 217 aria-label="Previous results page" 218 > 219 <${ChevronLeft} className="w-5 h-5" aria-hidden="true" /> 220 </button>
221 <span className="archive-pagination__status">Page ${currentPage} of ${totalPages}</span> 222 <button 223 type="button" 224 onClick=${() => onPageChange(currentPage + 1)} 225 disabled=${currentPage === totalPages} 226 className="archive-action archive-action--quiet archive-pagination__button" 227 aria-label="Next results page" 228 > 229 <${ChevronRight} className="w-5 h-5" aria-hidden="true" /> 230 </button> 231 </nav> 232 `} 233 </div> 234 `; 235}; 236 237export default ArchiveResults;
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.