@@ -14,6 +14,8 @@ import { updateUrl } from './url.js';
1414
1515// ---- Filter Sidebar ---------------------------------------------------------
1616
17+ export const FILTER_TOP_N = 8 ;
18+
1719export function collectDistinctValues ( field , isArray ) {
1820 const values = new Set ( ) ;
1921 for ( const entry of state . data ) {
@@ -26,6 +28,35 @@ export function collectDistinctValues(field, isArray) {
2628 return [ ...values ] . sort ( ) ;
2729}
2830
31+ // Returns the top n values for a field sorted by descending frequency across
32+ // all data entries. Ties are broken alphabetically.
33+ export function collectTopValues ( field , isArray , n ) {
34+ const counts = new Map ( ) ;
35+ for ( const entry of state . data ) {
36+ const vals = isArray
37+ ? entry [ field ] || [ ]
38+ : entry [ field ] != null && entry [ field ] !== ''
39+ ? [ entry [ field ] ]
40+ : [ ] ;
41+ for ( const v of vals ) {
42+ counts . set ( v , ( counts . get ( v ) || 0 ) + 1 ) ;
43+ }
44+ }
45+ return [ ...counts . entries ( ) ]
46+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] || a [ 0 ] . localeCompare ( b [ 0 ] ) )
47+ . slice ( 0 , n )
48+ . map ( ( [ v ] ) => v ) ;
49+ }
50+
51+ // Returns topValues with any currently-selected values that didn't make the
52+ // top-N appended at the end, so active filters are always visible.
53+ function defaultVisibleValues ( topValues , allValues , filterKey ) {
54+ const selected = state . filters [ filterKey ] || [ ] ;
55+ const topSet = new Set ( topValues ) ;
56+ const extras = selected . filter ( ( v ) => ! topSet . has ( v ) && allValues . includes ( v ) ) ;
57+ return [ ...topValues , ...extras ] ;
58+ }
59+
2960export function buildPills ( container , values , filterKey , labelFn ) {
3061 container . innerHTML = '' ;
3162 const selected = state . filters [ filterKey ] || [ ] ;
@@ -55,34 +86,49 @@ export function renderFilterSidebar() {
5586 for ( const { id, key, isArray, fn, pinTop } of fields ) {
5687 const container = document . getElementById ( id ) ;
5788 if ( ! container ) continue ;
58- let values = collectDistinctValues ( key , isArray ) ;
89+ let allValues = collectDistinctValues ( key , isArray ) ;
90+ if ( pinTop ) {
91+ allValues = [ ...pinTop . filter ( ( v ) => allValues . includes ( v ) ) , ...allValues . filter ( ( v ) => ! pinTop . includes ( v ) ) ] ;
92+ }
93+ let topValues = collectTopValues ( key , isArray , FILTER_TOP_N ) ;
5994 if ( pinTop ) {
60- values = [ ...pinTop . filter ( ( v ) => values . includes ( v ) ) , ...values . filter ( ( v ) => ! pinTop . includes ( v ) ) ] ;
95+ topValues = [ ...pinTop . filter ( ( v ) => topValues . includes ( v ) ) , ...topValues . filter ( ( v ) => ! pinTop . includes ( v ) ) ] ;
6196 }
62- buildPills ( container , values , key , fn ) ;
63- container . _allValues = values ;
97+ buildPills ( container , defaultVisibleValues ( topValues , allValues , key ) , key , fn ) ;
98+ container . _allValues = allValues ;
99+ container . _topValues = topValues ;
64100 container . _filterKey = key ;
65101 container . _labelFn = fn ;
102+ const searchInput = document . querySelector ( `.filter-search-within[data-target="${ id } "]` ) ;
103+ if ( searchInput ) searchInput . hidden = allValues . length <= FILTER_TOP_N ;
66104 }
67105
68106 // Searchable groups
69107 const publisherVals = collectDistinctValues ( 'publisher' , false ) ;
108+ const publisherTop = collectTopValues ( 'publisher' , false , FILTER_TOP_N ) ;
70109 const authorVals = collectDistinctValues ( 'authors' , true ) ;
110+ const authorTop = collectTopValues ( 'authors' , true , FILTER_TOP_N ) ;
71111
72112 const pubContainer = document . getElementById ( 'filter-publisher' ) ;
73113 if ( pubContainer ) {
74- buildPills ( pubContainer , publisherVals , 'publisher' , ( v ) => v ) ;
114+ buildPills ( pubContainer , defaultVisibleValues ( publisherTop , publisherVals , 'publisher' ) , 'publisher' , ( v ) => v ) ;
75115 pubContainer . _allValues = publisherVals ;
116+ pubContainer . _topValues = publisherTop ;
76117 pubContainer . _filterKey = 'publisher' ;
77118 pubContainer . _labelFn = ( v ) => v ;
119+ const pubSearch = document . querySelector ( '.filter-search-within[data-target="filter-publisher"]' ) ;
120+ if ( pubSearch ) pubSearch . hidden = publisherVals . length <= FILTER_TOP_N ;
78121 }
79122
80123 const authorContainer = document . getElementById ( 'filter-authors' ) ;
81124 if ( authorContainer ) {
82- buildPills ( authorContainer , authorVals , 'authors' , ( v ) => v ) ;
125+ buildPills ( authorContainer , defaultVisibleValues ( authorTop , authorVals , 'authors' ) , 'authors' , ( v ) => v ) ;
83126 authorContainer . _allValues = authorVals ;
127+ authorContainer . _topValues = authorTop ;
84128 authorContainer . _filterKey = 'authors' ;
85129 authorContainer . _labelFn = ( v ) => v ;
130+ const authorSearch = document . querySelector ( '.filter-search-within[data-target="filter-authors"]' ) ;
131+ if ( authorSearch ) authorSearch . hidden = authorVals . length <= FILTER_TOP_N ;
86132 }
87133
88134 syncFilterControlStates ( ) ;
@@ -124,12 +170,14 @@ export function renderResults() {
124170 paginationEl . innerHTML = '' ;
125171
126172 if ( entry ) {
173+ document . title = `${ entry . title } — Torchfinder` ;
127174 summary . textContent = entry . title ;
128175 list . innerHTML = renderCardHtml ( entry , true ) ;
129176 attachCardListeners ( list ) ;
130177 const heading = list . querySelector ( '.card-title' ) ;
131178 if ( heading ) requestAnimationFrame ( ( ) => heading . focus ( ) ) ;
132179 } else {
180+ document . title = 'Torchfinder' ;
133181 summary . textContent = '' ;
134182 list . innerHTML =
135183 '<div class="empty-state"><p>Entry not found.</p></div>' ;
@@ -138,6 +186,7 @@ export function renderResults() {
138186 return ;
139187 }
140188
189+ document . title = 'Torchfinder' ;
141190 backBtn . hidden = true ;
142191
143192 if ( ! state . data ) return ;
0 commit comments