@@ -7,8 +7,11 @@ const elements = {
77 add : document . querySelector ( "[data-sprites-add]" ) ,
88 apiStatus : document . querySelector ( "[data-sprites-api-status]" ) ,
99 count : document . querySelector ( "[data-sprites-count]" ) ,
10+ categoryFilter : document . querySelector ( "[data-sprites-category-filter]" ) ,
11+ clearFilters : document . querySelector ( "[data-sprites-clear-filters]" ) ,
1012 emptyState : document . querySelector ( "[data-sprites-empty-state]" ) ,
1113 errorState : document . querySelector ( "[data-sprites-error-state]" ) ,
14+ filterStatus : document . querySelector ( "[data-sprites-filter-status]" ) ,
1215 libraryStatus : document . querySelector ( "[data-sprites-library-status]" ) ,
1316 metadata : document . querySelector ( "[data-sprites-metadata]" ) ,
1417 outputStatus : document . querySelector ( "[data-sprites-output-status]" ) ,
@@ -19,8 +22,11 @@ const elements = {
1922 refresh : document . querySelector ( "[data-sprites-refresh]" ) ,
2023 replace : document . querySelector ( "[data-sprites-replace]" ) ,
2124 replaceStatus : document . querySelector ( "[data-sprites-replace-status]" ) ,
25+ search : document . querySelector ( "[data-sprites-search]" ) ,
2226 storageStatus : document . querySelector ( "[data-sprites-storage-status]" ) ,
27+ statusFilter : document . querySelector ( "[data-sprites-status-filter]" ) ,
2328 tableBody : document . querySelector ( "[data-sprites-table-body]" ) ,
29+ tagFilter : document . querySelector ( "[data-sprites-tag-filter]" ) ,
2430 updated : document . querySelector ( "[data-sprites-updated]" ) ,
2531 validation : document . querySelector ( "[data-sprites-validation]" ) ,
2632 duplicate : document . querySelector ( "[data-sprites-duplicate]" ) ,
@@ -172,6 +178,16 @@ function paletteKeysFor(sprite) {
172178 return [ ] ;
173179}
174180
181+ function tagKeysFor ( sprite ) {
182+ if ( Array . isArray ( sprite ?. tagKeys ) ) {
183+ return sprite . tagKeys . map ( ( key ) => String ( key || "" ) . trim ( ) ) . filter ( Boolean ) ;
184+ }
185+ if ( Array . isArray ( sprite ?. tag_keys ) ) {
186+ return sprite . tag_keys . map ( ( key ) => String ( key || "" ) . trim ( ) ) . filter ( Boolean ) ;
187+ }
188+ return [ ] ;
189+ }
190+
175191function usageCountFor ( sprite ) {
176192 const count = Number ( sprite ?. usageCount ?? sprite ?. usage_count ?? sprite ?. references ?. length ) ;
177193 return Number . isFinite ( count ) && count >= 0 ? String ( count ) : "0" ;
@@ -192,12 +208,93 @@ function spriteRowsFromPayload(payload) {
192208 return [ ] ;
193209}
194210
211+ function uniqueSorted ( values ) {
212+ return [ ...new Set ( values . map ( ( value ) => String ( value || "" ) . trim ( ) ) . filter ( Boolean ) ) ]
213+ . sort ( ( left , right ) => left . localeCompare ( right , undefined , { sensitivity : "base" } ) ) ;
214+ }
215+
216+ function setSelectOptions ( select , values , allLabel ) {
217+ if ( ! select ) {
218+ return ;
219+ }
220+ const current = select . value ;
221+ const options = [ "" ] . concat ( values ) ;
222+ select . replaceChildren ( ...options . map ( ( value ) => {
223+ const option = document . createElement ( "option" ) ;
224+ option . value = value ;
225+ option . textContent = value || allLabel ;
226+ return option ;
227+ } ) ) ;
228+ select . value = options . includes ( current ) ? current : "" ;
229+ }
230+
231+ function renderFilterOptions ( sprites ) {
232+ setSelectOptions ( elements . statusFilter , SPRITE_STATUSES , "All statuses" ) ;
233+ setSelectOptions ( elements . categoryFilter , uniqueSorted ( sprites . map ( ( sprite ) => sprite . category ) ) , "All categories" ) ;
234+ setSelectOptions ( elements . tagFilter , uniqueSorted ( sprites . flatMap ( tagKeysFor ) ) , "All tag keys" ) ;
235+ }
236+
237+ function filterValues ( ) {
238+ return {
239+ category : String ( elements . categoryFilter ?. value || "" ) . trim ( ) ,
240+ search : String ( elements . search ?. value || "" ) . trim ( ) . toLowerCase ( ) ,
241+ status : String ( elements . statusFilter ?. value || "" ) . trim ( ) ,
242+ tagKey : String ( elements . tagFilter ?. value || "" ) . trim ( ) ,
243+ } ;
244+ }
245+
246+ function spriteMatchesFilters ( sprite , filters ) {
247+ if ( filters . status && sprite . status !== filters . status ) {
248+ return false ;
249+ }
250+ if ( filters . category && sprite . category !== filters . category ) {
251+ return false ;
252+ }
253+ if ( filters . tagKey && ! tagKeysFor ( sprite ) . includes ( filters . tagKey ) ) {
254+ return false ;
255+ }
256+ if ( ! filters . search ) {
257+ return true ;
258+ }
259+ const haystack = [
260+ sprite . name ,
261+ sprite . status ,
262+ sprite . category ,
263+ sprite . source ,
264+ sprite . storagePath ,
265+ ...tagKeysFor ( sprite ) ,
266+ ...paletteKeysFor ( sprite ) ,
267+ ] . map ( ( value ) => String ( value || "" ) . toLowerCase ( ) ) . join ( " " ) ;
268+ return haystack . includes ( filters . search ) ;
269+ }
270+
271+ function filteredSprites ( ) {
272+ const filters = filterValues ( ) ;
273+ return currentSprites . filter ( ( sprite ) => spriteMatchesFilters ( sprite , filters ) ) ;
274+ }
275+
276+ function renderFilterStatus ( visibleCount , totalCount ) {
277+ if ( totalCount === 0 ) {
278+ setText ( elements . filterStatus , "No API-backed Sprites records are available to filter." ) ;
279+ return ;
280+ }
281+ const filters = filterValues ( ) ;
282+ const activeFilters = Object . values ( filters ) . filter ( Boolean ) . length ;
283+ setText (
284+ elements . filterStatus ,
285+ activeFilters > 0
286+ ? `${ visibleCount } of ${ totalCount } Sprites records match current filters.`
287+ : `${ totalCount } Sprites records available.`
288+ ) ;
289+ }
290+
195291function renderLoading ( ) {
196292 setText ( elements . apiStatus , "Loading" ) ;
197293 setText ( elements . libraryStatus , "Loading" ) ;
198294 setText ( elements . outputStatus , "Loading" ) ;
199295 setText ( elements . outputSummary , "Waiting for Sprites API response." ) ;
200296 setActionStatus ( "Loading Sprites records." ) ;
297+ setText ( elements . filterStatus , "Filters load with API-backed Sprites records." ) ;
201298 setText ( elements . storageStatus , "Storage import is checking API capabilities." ) ;
202299 setText ( elements . replaceStatus , "Select a sprite to update source metadata through the API." ) ;
203300 setText ( elements . emptyState , "Loading Sprites records." ) ;
@@ -226,6 +323,7 @@ function renderUnavailable(message) {
226323 setText ( elements . paletteStatus , "Palette/Colors references unavailable until Sprites records load from the API." ) ;
227324 setText ( elements . paletteSelectionStatus , "Palette/Colors selection unavailable until API-backed key records are available." ) ;
228325 setText ( elements . storageStatus , "Storage import unavailable because the Sprites API is not responding." ) ;
326+ setText ( elements . filterStatus , "Filters unavailable until Sprites records load from the API." ) ;
229327 setText ( elements . replaceStatus , "Replace metadata unavailable until the Sprites API responds." ) ;
230328 renderPreviewPanel ( null ) ;
231329 setText ( elements . updated , new Date ( ) . toLocaleTimeString ( ) ) ;
@@ -316,13 +414,13 @@ function selectSprite(sprite) {
316414 renderPreviewPanel ( sprite ) ;
317415}
318416
319- function renderRows ( sprites ) {
417+ function renderRows ( sprites , emptyMessage = "No Sprites records returned by the API." ) {
320418 if ( ! elements . tableBody ) {
321419 return ;
322420 }
323421 if ( sprites . length === 0 ) {
324422 const row = document . createElement ( "tr" ) ;
325- const cell = createCell ( "No Sprites records returned by the API." ) ;
423+ const cell = createCell ( emptyMessage ) ;
326424 cell . colSpan = 9 ;
327425 row . append ( cell ) ;
328426 elements . tableBody . replaceChildren ( ...( editingKey === "__new__" ? [ createEditRow ( ) , row ] : [ row ] ) ) ;
@@ -425,11 +523,13 @@ function renderSprites(payload) {
425523 const sprites = spriteRowsFromPayload ( payload ) ;
426524 currentSprites = sprites ;
427525 const count = sprites . length ;
526+ renderFilterOptions ( sprites ) ;
527+ const visibleSprites = filteredSprites ( ) ;
428528 setText ( elements . apiStatus , "Ready" ) ;
429529 setText ( elements . libraryStatus , count > 0 ? "Ready" : "Empty" ) ;
430530 setText ( elements . count , String ( count ) ) ;
431531 setText ( elements . outputStatus , count > 0 ? "Ready" : "Empty" ) ;
432- setText ( elements . outputSummary , count > 0 ? `${ count } sprite record${ count === 1 ? "" : "s" } loaded from the API.` : "Sprites API responded with no records." ) ;
532+ setText ( elements . outputSummary , count > 0 ? `${ visibleSprites . length } of ${ count } sprite record${ count === 1 ? "" : "s" } displayed from the API.` : "Sprites API responded with no records." ) ;
433533 setText ( elements . emptyState , count > 0 ? "" : "No Sprites records returned by the API." ) ;
434534 setText ( elements . updated , new Date ( ) . toLocaleTimeString ( ) ) ;
435535 setText ( elements . metadata , count > 0 ? "Select a sprite row to review its metadata." : "No sprite metadata available yet." ) ;
@@ -440,7 +540,11 @@ function renderSprites(payload) {
440540 setHidden ( elements . emptyState , count > 0 ) ;
441541 setHidden ( elements . errorState , true ) ;
442542 renderPaletteStatus ( sprites ) ;
443- renderRows ( sprites ) ;
543+ renderFilterStatus ( visibleSprites . length , count ) ;
544+ renderRows (
545+ visibleSprites ,
546+ count > 0 ? "No Sprites records match current filters." : "No Sprites records returned by the API."
547+ ) ;
444548}
445549
446550function bodyFromSprite ( sprite , overrides = { } ) {
@@ -648,7 +752,7 @@ elements.refresh?.addEventListener("click", () => {
648752
649753elements . add ?. addEventListener ( "click" , ( ) => {
650754 editingKey = "__new__" ;
651- renderRows ( currentSprites ) ;
755+ renderRows ( filteredSprites ( ) ) ;
652756 setActionStatus ( "New sprite row ready. Name and status are required." ) ;
653757} ) ;
654758
@@ -665,13 +769,13 @@ elements.tableBody?.addEventListener("click", (event) => {
665769 const duplicateKey = target . dataset . spritesDuplicateRow ;
666770 if ( editKey !== undefined ) {
667771 editingKey = editKey ;
668- renderRows ( currentSprites ) ;
772+ renderRows ( filteredSprites ( ) ) ;
669773 setActionStatus ( "Editing sprite row. Name and status are required." ) ;
670774 return ;
671775 }
672776 if ( cancelKey !== undefined ) {
673777 editingKey = "" ;
674- renderRows ( currentSprites ) ;
778+ renderRows ( filteredSprites ( ) ) ;
675779 setActionStatus ( "Sprite edit cancelled." ) ;
676780 return ;
677781 }
@@ -703,4 +807,37 @@ elements.replace?.addEventListener("click", () => {
703807 void replaceSpriteMetadata ( selectedSpriteKey ) ;
704808} ) ;
705809
810+ [ elements . search , elements . statusFilter , elements . categoryFilter , elements . tagFilter ] . forEach ( ( control ) => {
811+ control ?. addEventListener ( "input" , ( ) => {
812+ editingKey = "" ;
813+ const visibleSprites = filteredSprites ( ) ;
814+ renderFilterStatus ( visibleSprites . length , currentSprites . length ) ;
815+ renderRows ( visibleSprites , "No Sprites records match current filters." ) ;
816+ } ) ;
817+ control ?. addEventListener ( "change" , ( ) => {
818+ editingKey = "" ;
819+ const visibleSprites = filteredSprites ( ) ;
820+ renderFilterStatus ( visibleSprites . length , currentSprites . length ) ;
821+ renderRows ( visibleSprites , "No Sprites records match current filters." ) ;
822+ } ) ;
823+ } ) ;
824+
825+ elements . clearFilters ?. addEventListener ( "click" , ( ) => {
826+ if ( elements . search ) {
827+ elements . search . value = "" ;
828+ }
829+ if ( elements . statusFilter ) {
830+ elements . statusFilter . value = "" ;
831+ }
832+ if ( elements . categoryFilter ) {
833+ elements . categoryFilter . value = "" ;
834+ }
835+ if ( elements . tagFilter ) {
836+ elements . tagFilter . value = "" ;
837+ }
838+ const visibleSprites = filteredSprites ( ) ;
839+ renderFilterStatus ( visibleSprites . length , currentSprites . length ) ;
840+ renderRows ( visibleSprites ) ;
841+ } ) ;
842+
706843void loadSprites ( ) ;
0 commit comments