diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js index fcbe5bd04..9924f47cc 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js @@ -7,8 +7,11 @@ const elements = { add: document.querySelector("[data-sprites-add]"), apiStatus: document.querySelector("[data-sprites-api-status]"), count: document.querySelector("[data-sprites-count]"), + categoryFilter: document.querySelector("[data-sprites-category-filter]"), + clearFilters: document.querySelector("[data-sprites-clear-filters]"), emptyState: document.querySelector("[data-sprites-empty-state]"), errorState: document.querySelector("[data-sprites-error-state]"), + filterStatus: document.querySelector("[data-sprites-filter-status]"), libraryStatus: document.querySelector("[data-sprites-library-status]"), metadata: document.querySelector("[data-sprites-metadata]"), outputStatus: document.querySelector("[data-sprites-output-status]"), @@ -19,8 +22,11 @@ const elements = { refresh: document.querySelector("[data-sprites-refresh]"), replace: document.querySelector("[data-sprites-replace]"), replaceStatus: document.querySelector("[data-sprites-replace-status]"), + search: document.querySelector("[data-sprites-search]"), storageStatus: document.querySelector("[data-sprites-storage-status]"), + statusFilter: document.querySelector("[data-sprites-status-filter]"), tableBody: document.querySelector("[data-sprites-table-body]"), + tagFilter: document.querySelector("[data-sprites-tag-filter]"), updated: document.querySelector("[data-sprites-updated]"), validation: document.querySelector("[data-sprites-validation]"), duplicate: document.querySelector("[data-sprites-duplicate]"), @@ -172,6 +178,16 @@ function paletteKeysFor(sprite) { return []; } +function tagKeysFor(sprite) { + if (Array.isArray(sprite?.tagKeys)) { + return sprite.tagKeys.map((key) => String(key || "").trim()).filter(Boolean); + } + if (Array.isArray(sprite?.tag_keys)) { + return sprite.tag_keys.map((key) => String(key || "").trim()).filter(Boolean); + } + return []; +} + function usageCountFor(sprite) { const count = Number(sprite?.usageCount ?? sprite?.usage_count ?? sprite?.references?.length); return Number.isFinite(count) && count >= 0 ? String(count) : "0"; @@ -192,12 +208,93 @@ function spriteRowsFromPayload(payload) { return []; } +function uniqueSorted(values) { + return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))] + .sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" })); +} + +function setSelectOptions(select, values, allLabel) { + if (!select) { + return; + } + const current = select.value; + const options = [""].concat(values); + select.replaceChildren(...options.map((value) => { + const option = document.createElement("option"); + option.value = value; + option.textContent = value || allLabel; + return option; + })); + select.value = options.includes(current) ? current : ""; +} + +function renderFilterOptions(sprites) { + setSelectOptions(elements.statusFilter, SPRITE_STATUSES, "All statuses"); + setSelectOptions(elements.categoryFilter, uniqueSorted(sprites.map((sprite) => sprite.category)), "All categories"); + setSelectOptions(elements.tagFilter, uniqueSorted(sprites.flatMap(tagKeysFor)), "All tag keys"); +} + +function filterValues() { + return { + category: String(elements.categoryFilter?.value || "").trim(), + search: String(elements.search?.value || "").trim().toLowerCase(), + status: String(elements.statusFilter?.value || "").trim(), + tagKey: String(elements.tagFilter?.value || "").trim(), + }; +} + +function spriteMatchesFilters(sprite, filters) { + if (filters.status && sprite.status !== filters.status) { + return false; + } + if (filters.category && sprite.category !== filters.category) { + return false; + } + if (filters.tagKey && !tagKeysFor(sprite).includes(filters.tagKey)) { + return false; + } + if (!filters.search) { + return true; + } + const haystack = [ + sprite.name, + sprite.status, + sprite.category, + sprite.source, + sprite.storagePath, + ...tagKeysFor(sprite), + ...paletteKeysFor(sprite), + ].map((value) => String(value || "").toLowerCase()).join(" "); + return haystack.includes(filters.search); +} + +function filteredSprites() { + const filters = filterValues(); + return currentSprites.filter((sprite) => spriteMatchesFilters(sprite, filters)); +} + +function renderFilterStatus(visibleCount, totalCount) { + if (totalCount === 0) { + setText(elements.filterStatus, "No API-backed Sprites records are available to filter."); + return; + } + const filters = filterValues(); + const activeFilters = Object.values(filters).filter(Boolean).length; + setText( + elements.filterStatus, + activeFilters > 0 + ? `${visibleCount} of ${totalCount} Sprites records match current filters.` + : `${totalCount} Sprites records available.` + ); +} + function renderLoading() { setText(elements.apiStatus, "Loading"); setText(elements.libraryStatus, "Loading"); setText(elements.outputStatus, "Loading"); setText(elements.outputSummary, "Waiting for Sprites API response."); setActionStatus("Loading Sprites records."); + setText(elements.filterStatus, "Filters load with API-backed Sprites records."); setText(elements.storageStatus, "Storage import is checking API capabilities."); setText(elements.replaceStatus, "Select a sprite to update source metadata through the API."); setText(elements.emptyState, "Loading Sprites records."); @@ -226,6 +323,7 @@ function renderUnavailable(message) { setText(elements.paletteStatus, "Palette/Colors references unavailable until Sprites records load from the API."); setText(elements.paletteSelectionStatus, "Palette/Colors selection unavailable until API-backed key records are available."); setText(elements.storageStatus, "Storage import unavailable because the Sprites API is not responding."); + setText(elements.filterStatus, "Filters unavailable until Sprites records load from the API."); setText(elements.replaceStatus, "Replace metadata unavailable until the Sprites API responds."); renderPreviewPanel(null); setText(elements.updated, new Date().toLocaleTimeString()); @@ -316,13 +414,13 @@ function selectSprite(sprite) { renderPreviewPanel(sprite); } -function renderRows(sprites) { +function renderRows(sprites, emptyMessage = "No Sprites records returned by the API.") { if (!elements.tableBody) { return; } if (sprites.length === 0) { const row = document.createElement("tr"); - const cell = createCell("No Sprites records returned by the API."); + const cell = createCell(emptyMessage); cell.colSpan = 9; row.append(cell); elements.tableBody.replaceChildren(...(editingKey === "__new__" ? [createEditRow(), row] : [row])); @@ -425,11 +523,13 @@ function renderSprites(payload) { const sprites = spriteRowsFromPayload(payload); currentSprites = sprites; const count = sprites.length; + renderFilterOptions(sprites); + const visibleSprites = filteredSprites(); setText(elements.apiStatus, "Ready"); setText(elements.libraryStatus, count > 0 ? "Ready" : "Empty"); setText(elements.count, String(count)); setText(elements.outputStatus, count > 0 ? "Ready" : "Empty"); - setText(elements.outputSummary, count > 0 ? `${count} sprite record${count === 1 ? "" : "s"} loaded from the API.` : "Sprites API responded with no records."); + setText(elements.outputSummary, count > 0 ? `${visibleSprites.length} of ${count} sprite record${count === 1 ? "" : "s"} displayed from the API.` : "Sprites API responded with no records."); setText(elements.emptyState, count > 0 ? "" : "No Sprites records returned by the API."); setText(elements.updated, new Date().toLocaleTimeString()); 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) { setHidden(elements.emptyState, count > 0); setHidden(elements.errorState, true); renderPaletteStatus(sprites); - renderRows(sprites); + renderFilterStatus(visibleSprites.length, count); + renderRows( + visibleSprites, + count > 0 ? "No Sprites records match current filters." : "No Sprites records returned by the API." + ); } function bodyFromSprite(sprite, overrides = {}) { @@ -648,7 +752,7 @@ elements.refresh?.addEventListener("click", () => { elements.add?.addEventListener("click", () => { editingKey = "__new__"; - renderRows(currentSprites); + renderRows(filteredSprites()); setActionStatus("New sprite row ready. Name and status are required."); }); @@ -665,13 +769,13 @@ elements.tableBody?.addEventListener("click", (event) => { const duplicateKey = target.dataset.spritesDuplicateRow; if (editKey !== undefined) { editingKey = editKey; - renderRows(currentSprites); + renderRows(filteredSprites()); setActionStatus("Editing sprite row. Name and status are required."); return; } if (cancelKey !== undefined) { editingKey = ""; - renderRows(currentSprites); + renderRows(filteredSprites()); setActionStatus("Sprite edit cancelled."); return; } @@ -703,4 +807,37 @@ elements.replace?.addEventListener("click", () => { void replaceSpriteMetadata(selectedSpriteKey); }); +[elements.search, elements.statusFilter, elements.categoryFilter, elements.tagFilter].forEach((control) => { + control?.addEventListener("input", () => { + editingKey = ""; + const visibleSprites = filteredSprites(); + renderFilterStatus(visibleSprites.length, currentSprites.length); + renderRows(visibleSprites, "No Sprites records match current filters."); + }); + control?.addEventListener("change", () => { + editingKey = ""; + const visibleSprites = filteredSprites(); + renderFilterStatus(visibleSprites.length, currentSprites.length); + renderRows(visibleSprites, "No Sprites records match current filters."); + }); +}); + +elements.clearFilters?.addEventListener("click", () => { + if (elements.search) { + elements.search.value = ""; + } + if (elements.statusFilter) { + elements.statusFilter.value = ""; + } + if (elements.categoryFilter) { + elements.categoryFilter.value = ""; + } + if (elements.tagFilter) { + elements.tagFilter.value = ""; + } + const visibleSprites = filteredSprites(); + renderFilterStatus(visibleSprites.length, currentSprites.length); + renderRows(visibleSprites); +}); + void loadSprites(); diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md new file mode 100644 index 000000000..3b1d50f6e --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md @@ -0,0 +1,12 @@ +# PR_26177_CHARLIE_014 Branch Validation + +Status: PASS + +## Checks + +- PASS: PR014 was created as a stacked branch from `PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette`. +- PASS: Stacking is required because search/filter controls build on the PR013 Sprites table and metadata shell. +- PASS: Current work branch is `PR_26177_CHARLIE_014-sprites-tags-categories-search`. +- PASS: Branch contains only the Sprites tags/categories/search PR scope relative to PR013. +- PASS: No merge was performed. +- PASS: No `start_of_day` path is changed. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md new file mode 100644 index 000000000..fbd30a43f --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md @@ -0,0 +1,13 @@ +# PR_26177_CHARLIE_014 Manual Validation Notes + +Status: PASS + +## Manual Review + +- Verified Search filters by API-returned sprite fields and tag/palette key text. +- Verified category options are derived from current API records. +- Verified tag key options are derived from current API records. +- Verified status filtering uses the Sprites status contract. +- Verified clear filters restores the unfiltered API-backed table. +- Verified no static category or tag product-data list was introduced. +- Verified Sprites does not duplicate Tags ownership. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md new file mode 100644 index 000000000..aa85dda6e --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md @@ -0,0 +1,19 @@ +# PR_26177_CHARLIE_014 Requirements Checklist + +Status: PASS + +- PASS: Added search for Sprites. +- PASS: Added status filter. +- PASS: Added categories filter. +- PASS: Added tag key filter. +- PASS: Search and filters use API/database-backed sprite data. +- PASS: Categories are derived from API-backed records. +- PASS: Tag keys are derived from API-backed records. +- PASS: Did not create Sprite-owned Tags data. +- PASS: Did not use page-local product arrays for categories or tags. +- PASS: Added table filtering UX consistent with GFS patterns. +- PASS: Did not add browser storage product-data source of truth. +- PASS: Did not introduce MEM DB, local-mem, fake-login, or silent fallback. +- PASS: Targeted Playwright coverage passed. +- PASS: Required report artifacts were created. +- PASS: Repo-structured ZIP artifact was created under `tmp/`. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md new file mode 100644 index 000000000..f1c57be49 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md @@ -0,0 +1,33 @@ +# PR_26177_CHARLIE_014 Validation Lane + +Status: PASS + +## Commands + +```powershell +rg -n "" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js tests/playwright/tools/SpritesToolShell.spec.mjs +``` + +Result: PASS, no matches. + +```powershell +rg -n "localStorage|sessionStorage|indexedDB|imageDataUrl|MEM DB|local-mem|fake-login|silent fallback" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js tests/playwright/tools/SpritesToolShell.spec.mjs +``` + +Result: PASS, no matches. + +```powershell +git diff --check +``` + +Result: PASS. Git reported only repository line-ending warnings for changed HTML/test files. + +```powershell +node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list +``` + +Result: PASS, 9 passed. + +## Playwright Coverage + +Targeted Playwright coverage updated `docs_build/dev/reports/playwright_v8_coverage_report.txt` for the Sprites browser module. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md new file mode 100644 index 000000000..fb4161770 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md @@ -0,0 +1,45 @@ +# PR_26177_CHARLIE_014-sprites-tags-categories-search + +Team: Charlie + +Status: PASS + +## Scope + +Added API-backed search and filters for Sprites records. Categories and tag keys are derived from the API response; Sprites does not create a separate tag authority or page-local product data list. + +## Changed Files + +- `toolbox/sprites/index.html` +- `assets/toolbox/sprites/js/index.js` +- `tests/playwright/tools/SpritesToolShell.spec.mjs` +- `docs_build/dev/reports/playwright_v8_coverage_report.txt` +- `docs_build/dev/reports/codex_review.diff` +- `docs_build/dev/reports/codex_changed_files.txt` +- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md` +- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md` +- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md` +- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md` +- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md` + +## Implementation Notes + +- Added search control. +- Added status filter using the Sprites API status contract. +- Added category filter derived from API-backed sprite records. +- Added tag key filter derived from API-backed sprite records. +- Added clear filters action. +- Added filter status summary for visible records versus total API-backed records. +- Did not duplicate Tags ownership or add page-local reusable product arrays. + +## Validation + +- PASS: `git diff --check` +- PASS: inline CSS/script/handler scan for Sprites files found no matches. +- PASS: browser storage and forbidden local data pattern scan found no matches. +- PASS: no `start_of_day` files changed. +- PASS: `node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list` + +## ZIP Artifact + +- `tmp/PR_26177_CHARLIE_014-sprites-tags-categories-search_delta.zip` diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_branch-validation.md new file mode 100644 index 000000000..32db30b06 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_branch-validation.md @@ -0,0 +1,23 @@ +# PR_26177_CHARLIE_014-sprites-tags-categories-search + +Generated: 2026-06-26 +Team: Charlie +GitHub PR: #224 +Branch: PR_26177_CHARLIE_014-sprites-tags-categories-search +Base: PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette + +## Branch Validation + +| Check | Result | Notes | +| --- | --- | --- | +| Branch exists locally | PASS | PR_26177_CHARLIE_014-sprites-tags-categories-search | +| Branch pushed to origin | PASS | Existing upstream branch is present. | +| Worktree clean before report generation | PASS | Verified before companion reports were written. | +| Runtime/UI/API/database change in this report commit | PASS | None; report-only completion. | +| start_of_day changes | PASS | None. | +| GitHub draft state | PENDING | PR remains draft until Owner review. | +| GitHub mergeable state at EOD snapshot | PASS | mergeable=true; not merged because no Owner approval was provided. | + +## Result + +Report artifact completion is PASS. Merge readiness remains pending Owner review and dependency-order workflow. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_manual-validation-notes.md new file mode 100644 index 000000000..56771d595 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_manual-validation-notes.md @@ -0,0 +1,19 @@ +# PR_26177_CHARLIE_014-sprites-tags-categories-search + +Generated: 2026-06-26 +Team: Charlie +GitHub PR: #224 +Branch: PR_26177_CHARLIE_014-sprites-tags-categories-search +Base: PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette + +## Manual Validation Notes + +- PR scope: Adds API-backed search and filters for status, category, and tag key. +- Manual review should verify the PR in dependency order after prior Sprites branches are approved or merged. +- Browser-owned product data, page-local reusable color arrays, and silent fallback behavior should remain absent. +- Palette/Colors must remain the authoritative owner of reusable colors; Sprites may reference color keys only. +- Because this EOD update is report-only, no new manual UI behavior was introduced. + +## EOD Status + +Open draft PR. Not merged. Awaiting Owner review and dependency-order workflow. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_requirements-checklist.md new file mode 100644 index 000000000..a748a8f09 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_requirements-checklist.md @@ -0,0 +1,24 @@ +# PR_26177_CHARLIE_014-sprites-tags-categories-search + +Generated: 2026-06-26 +Team: Charlie +GitHub PR: #224 +Branch: PR_26177_CHARLIE_014-sprites-tags-categories-search +Base: PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette + +## Requirement Checklist + +| Requirement | Result | Notes | +| --- | --- | --- | +| One PR purpose only | PASS | Adds API-backed search and filters for status, category, and tag key. | +| Theme V2 / current GFS patterns preserved | PASS | Validated in original PR lane where UI was touched. | +| Browser -> API -> Database flow preserved | PASS | No browser-owned authoritative product data added. | +| Palette/Colors remains color SSoT | PASS | Sprites references Palette/Colors keys only where applicable. | +| No MEM DB/local-mem/fake-login/silent fallback introduced | PASS | No forbidden runtime patterns introduced in original PR scope. | +| No start_of_day changes | PASS | Confirmed for report completion. | +| Required companion reports present | PASS | Branch validation, checklist, validation lane, and manual notes added by this EOD report-only commit. | +| Repo ZIP under tmp | PASS | Local EOD ZIP was regenerated under tmp for this PR. | + +## Result + +PASS for report completion. PR remains unmerged pending review/approval. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_validation-lane.md new file mode 100644 index 000000000..873854593 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search_validation-lane.md @@ -0,0 +1,26 @@ +# PR_26177_CHARLIE_014-sprites-tags-categories-search + +Generated: 2026-06-26 +Team: Charlie +GitHub PR: #224 +Branch: PR_26177_CHARLIE_014-sprites-tags-categories-search +Base: PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette + +## Validation Lane + +Original targeted validation recorded by the PR: + +- git diff --check +- inline style/script/handler scan +- browser storage/forbidden local data scan +- Playwright SpritesToolShell (9 passed) + +EOD report-completion validation: + +- PASS: report-only changed-file intent. +- PASS: no source/runtime files changed by this companion report commit. +- PASS: ZIP artifact regenerated locally under tmp. + +## Result + +PASS for documented targeted validation. No additional runtime validation was run during EOD report generation because no implementation files changed. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 9ad5ceb61..c9162e9eb 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -3,22 +3,22 @@ M assets/toolbox/sprites/js/index.js M docs_build/dev/reports/playwright_v8_coverage_report.txt M tests/playwright/tools/SpritesToolShell.spec.mjs M toolbox/sprites/index.html -?? docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-branch-validation.md -?? docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-manual-validation-notes.md -?? docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-requirements-checklist.md -?? docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-validation-lane.md -?? docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette.md +?? docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md +?? docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md +?? docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md +?? docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md +?? docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md # git ls-files --others --exclude-standard -docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-branch-validation.md -docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-manual-validation-notes.md -docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-requirements-checklist.md -docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-validation-lane.md -docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette.md +docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md +docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md +docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md +docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md +docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md # git diff --stat -assets/toolbox/sprites/js/index.js | 183 ++++++++++++++++++++- +assets/toolbox/sprites/js/index.js | 151 ++++++++++++++++++++- .../dev/reports/playwright_v8_coverage_report.txt | 4 +- - tests/playwright/tools/SpritesToolShell.spec.mjs | 138 ++++++++++++++++ + tests/playwright/tools/SpritesToolShell.spec.mjs | 71 ++++++++++ toolbox/sprites/index.html | 15 ++ - 4 files changed, 334 insertions(+), 6 deletions(-) \ No newline at end of file + 4 files changed, 232 insertions(+), 9 deletions(-) \ No newline at end of file diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 3798c9276..4cd0466cd 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,330 +1,286 @@ diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js -index 471c64d83..fcbe5bd04 100644 +index fcbe5bd04..9924f47cc 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js -@@ -13,15 +13,22 @@ const elements = { +@@ -7,8 +7,11 @@ const elements = { + add: document.querySelector("[data-sprites-add]"), + apiStatus: document.querySelector("[data-sprites-api-status]"), + count: document.querySelector("[data-sprites-count]"), ++ categoryFilter: document.querySelector("[data-sprites-category-filter]"), ++ clearFilters: document.querySelector("[data-sprites-clear-filters]"), + emptyState: document.querySelector("[data-sprites-empty-state]"), + errorState: document.querySelector("[data-sprites-error-state]"), ++ filterStatus: document.querySelector("[data-sprites-filter-status]"), + libraryStatus: document.querySelector("[data-sprites-library-status]"), metadata: document.querySelector("[data-sprites-metadata]"), outputStatus: document.querySelector("[data-sprites-output-status]"), - outputSummary: document.querySelector("[data-sprites-output-summary]"), -+ paletteSelectionStatus: document.querySelector("[data-sprites-palette-selection-status]"), - paletteStatus: document.querySelector("[data-sprites-palette-status]"), -+ previewPanel: document.querySelector("[data-sprites-preview-panel]"), +@@ -19,8 +22,11 @@ const elements = { refresh: document.querySelector("[data-sprites-refresh]"), -+ replace: document.querySelector("[data-sprites-replace]"), -+ replaceStatus: document.querySelector("[data-sprites-replace-status]"), -+ storageStatus: document.querySelector("[data-sprites-storage-status]"), + replace: document.querySelector("[data-sprites-replace]"), + replaceStatus: document.querySelector("[data-sprites-replace-status]"), ++ search: document.querySelector("[data-sprites-search]"), + storageStatus: document.querySelector("[data-sprites-storage-status]"), ++ statusFilter: document.querySelector("[data-sprites-status-filter]"), tableBody: document.querySelector("[data-sprites-table-body]"), ++ tagFilter: document.querySelector("[data-sprites-tag-filter]"), updated: document.querySelector("[data-sprites-updated]"), validation: document.querySelector("[data-sprites-validation]"), -+ duplicate: document.querySelector("[data-sprites-duplicate]"), - }; - - let currentSprites = []; - let editingKey = ""; -+let selectedSpriteKey = ""; - - function setText(target, value) { - if (target) { -@@ -35,12 +42,27 @@ function setHidden(target, hidden) { - } + duplicate: document.querySelector("[data-sprites-duplicate]"), +@@ -172,6 +178,16 @@ function paletteKeysFor(sprite) { + return []; } -+function setDisabled(target, disabled) { -+ if (target) { -+ target.disabled = disabled; ++function tagKeysFor(sprite) { ++ if (Array.isArray(sprite?.tagKeys)) { ++ return sprite.tagKeys.map((key) => String(key || "").trim()).filter(Boolean); ++ } ++ if (Array.isArray(sprite?.tag_keys)) { ++ return sprite.tag_keys.map((key) => String(key || "").trim()).filter(Boolean); + } ++ return []; +} + - function createCell(value) { - const cell = document.createElement("td"); - cell.textContent = value; - return cell; + function usageCountFor(sprite) { + const count = Number(sprite?.usageCount ?? sprite?.usage_count ?? sprite?.references?.length); + return Number.isFinite(count) && count >= 0 ? String(count) : "0"; +@@ -192,12 +208,93 @@ function spriteRowsFromPayload(payload) { + return []; } -+function createParagraph(value, className = "") { -+ const paragraph = document.createElement("p"); -+ if (className) { -+ paragraph.className = className; -+ } -+ paragraph.textContent = value; -+ return paragraph; ++function uniqueSorted(values) { ++ return [...new Set(values.map((value) => String(value || "").trim()).filter(Boolean))] ++ .sort((left, right) => left.localeCompare(right, undefined, { sensitivity: "base" })); +} + - function createHeaderCell(value) { - const cell = document.createElement("th"); - cell.scope = "row"; -@@ -132,6 +154,14 @@ function formatSource(sprite) { - return normalizeText(sprite?.sourceName || sprite?.sourcePath || sprite?.storagePath || sprite?.storageKey || sprite?.sourceStorageReference); - } - -+function previewSourceFor(sprite) { -+ const source = String(sprite?.storagePath || sprite?.source || sprite?.sourcePath || sprite?.sourceName || "").trim(); -+ if (!source || /^https?:\/\//i.test(source)) { -+ return ""; ++function setSelectOptions(select, values, allLabel) { ++ if (!select) { ++ return; + } -+ return source; ++ const current = select.value; ++ const options = [""].concat(values); ++ select.replaceChildren(...options.map((value) => { ++ const option = document.createElement("option"); ++ option.value = value; ++ option.textContent = value || allLabel; ++ return option; ++ })); ++ select.value = options.includes(current) ? current : ""; +} + - function paletteKeysFor(sprite) { - if (Array.isArray(sprite?.paletteColorKeys)) { - return sprite.paletteColorKeys.map((key) => String(key || "").trim()).filter(Boolean); -@@ -168,6 +198,8 @@ function renderLoading() { - setText(elements.outputStatus, "Loading"); - setText(elements.outputSummary, "Waiting for Sprites API response."); - setActionStatus("Loading Sprites records."); -+ setText(elements.storageStatus, "Storage import is checking API capabilities."); -+ setText(elements.replaceStatus, "Select a sprite to update source metadata through the API."); - setText(elements.emptyState, "Loading Sprites records."); - setText(elements.updated, "Checking"); - setHidden(elements.emptyState, false); -@@ -192,6 +224,10 @@ function renderUnavailable(message) { - setText(elements.errorState, detail); - setText(elements.metadata, "Sprite metadata unavailable until the Sprites API responds."); - setText(elements.paletteStatus, "Palette/Colors references unavailable until Sprites records load from the API."); -+ setText(elements.paletteSelectionStatus, "Palette/Colors selection unavailable until API-backed key records are available."); -+ setText(elements.storageStatus, "Storage import unavailable because the Sprites API is not responding."); -+ setText(elements.replaceStatus, "Replace metadata unavailable until the Sprites API responds."); -+ renderPreviewPanel(null); - setText(elements.updated, new Date().toLocaleTimeString()); - setHidden(elements.emptyState, false); - setHidden(elements.errorState, false); -@@ -211,9 +247,73 @@ function renderPaletteStatus(sprites) { - }); - if (referencedKeys.size === 0) { - setText(elements.paletteStatus, "No Palette/Colors references in current Sprites records."); -+ setText(elements.paletteSelectionStatus, "Palette/Colors selection unavailable: no API-backed Palette/Colors key records are attached to Sprites yet."); - return; - } - setText(elements.paletteStatus, `${referencedKeys.size} Palette/Colors key reference${referencedKeys.size === 1 ? "" : "s"} surfaced from API records.`); -+ setText(elements.paletteSelectionStatus, "Palette/Colors key references are display-only until the Palette/Colors selection API is available."); ++function renderFilterOptions(sprites) { ++ setSelectOptions(elements.statusFilter, SPRITE_STATUSES, "All statuses"); ++ setSelectOptions(elements.categoryFilter, uniqueSorted(sprites.map((sprite) => sprite.category)), "All categories"); ++ setSelectOptions(elements.tagFilter, uniqueSorted(sprites.flatMap(tagKeysFor)), "All tag keys"); +} + -+function renderPreviewPanel(sprite) { -+ if (!elements.previewPanel) { -+ return; ++function filterValues() { ++ return { ++ category: String(elements.categoryFilter?.value || "").trim(), ++ search: String(elements.search?.value || "").trim().toLowerCase(), ++ status: String(elements.statusFilter?.value || "").trim(), ++ tagKey: String(elements.tagFilter?.value || "").trim(), ++ }; ++} ++ ++function spriteMatchesFilters(sprite, filters) { ++ if (filters.status && sprite.status !== filters.status) { ++ return false; + } -+ elements.previewPanel.replaceChildren(); -+ if (!sprite) { -+ elements.previewPanel.append(createParagraph("No sprite selected for preview.", "status")); -+ setDisabled(elements.duplicate, true); -+ setDisabled(elements.replace, true); -+ return; ++ if (filters.category && sprite.category !== filters.category) { ++ return false; ++ } ++ if (filters.tagKey && !tagKeysFor(sprite).includes(filters.tagKey)) { ++ return false; + } -+ const source = previewSourceFor(sprite); -+ if (source) { -+ const image = document.createElement("img"); -+ image.src = source; -+ image.alt = `${normalizeText(sprite.name)} preview`; -+ image.loading = "lazy"; -+ elements.previewPanel.append(image); -+ } else { -+ elements.previewPanel.append(createParagraph("Image preview unavailable for this sprite source.", "status")); ++ if (!filters.search) { ++ return true; + } ++ const haystack = [ ++ sprite.name, ++ sprite.status, ++ sprite.category, ++ sprite.source, ++ sprite.storagePath, ++ ...tagKeysFor(sprite), ++ ...paletteKeysFor(sprite), ++ ].map((value) => String(value || "").toLowerCase()).join(" "); ++ return haystack.includes(filters.search); ++} + -+ const metadata = document.createElement("div"); -+ metadata.className = "table-wrapper"; -+ const table = document.createElement("table"); -+ table.className = "data-table"; -+ table.setAttribute("aria-label", "Selected sprite metadata"); -+ const body = document.createElement("tbody"); -+ [ -+ ["File/Source", formatSource(sprite)], -+ ["MIME/Type", normalizeText(sprite.mimeType ?? sprite.mime_type)], -+ ["Dimensions", formatDimensions(sprite)], -+ ["File Size", normalizeText(sprite.sizeBytes ?? sprite.size_bytes, "Unavailable")], -+ ["Updated At", formatTimestamp(sprite.updatedAt ?? sprite.updated_at)], -+ ["Updated By", normalizeText(sprite.updatedBy ?? sprite.updated_by)], -+ ["Palette Keys", paletteKeysFor(sprite).join(", ") || "None"], -+ ].forEach(([label, value]) => { -+ const row = document.createElement("tr"); -+ row.append(createHeaderCell(label), createCell(value)); -+ body.append(row); -+ }); -+ table.append(body); -+ metadata.append(table); -+ elements.previewPanel.append(metadata); -+ setDisabled(elements.duplicate, false); -+ setDisabled(elements.replace, false); ++function filteredSprites() { ++ const filters = filterValues(); ++ return currentSprites.filter((sprite) => spriteMatchesFilters(sprite, filters)); +} + -+function selectSprite(sprite) { -+ selectedSpriteKey = sprite?.key || ""; -+ if (!sprite) { -+ setText(elements.metadata, "Select a sprite row to review its metadata."); -+ renderPreviewPanel(null); ++function renderFilterStatus(visibleCount, totalCount) { ++ if (totalCount === 0) { ++ setText(elements.filterStatus, "No API-backed Sprites records are available to filter."); + return; + } -+ const key = normalizeText(sprite?.key, "Unavailable"); -+ const mimeType = normalizeText(sprite?.mimeType ?? sprite?.mime_type, "Unavailable"); -+ const sizeBytes = normalizeText(sprite?.sizeBytes ?? sprite?.size_bytes, "Unavailable"); -+ setText(elements.metadata, `${normalizeText(sprite?.name)} (${key}) | ${mimeType} | ${formatDimensions(sprite)} | ${sizeBytes} bytes`); -+ renderPreviewPanel(sprite); ++ const filters = filterValues(); ++ const activeFilters = Object.values(filters).filter(Boolean).length; ++ setText( ++ elements.filterStatus, ++ activeFilters > 0 ++ ? `${visibleCount} of ${totalCount} Sprites records match current filters.` ++ : `${totalCount} Sprites records available.` ++ ); ++} ++ + function renderLoading() { + setText(elements.apiStatus, "Loading"); + setText(elements.libraryStatus, "Loading"); + setText(elements.outputStatus, "Loading"); + setText(elements.outputSummary, "Waiting for Sprites API response."); + setActionStatus("Loading Sprites records."); ++ setText(elements.filterStatus, "Filters load with API-backed Sprites records."); + setText(elements.storageStatus, "Storage import is checking API capabilities."); + setText(elements.replaceStatus, "Select a sprite to update source metadata through the API."); + setText(elements.emptyState, "Loading Sprites records."); +@@ -226,6 +323,7 @@ function renderUnavailable(message) { + setText(elements.paletteStatus, "Palette/Colors references unavailable until Sprites records load from the API."); + setText(elements.paletteSelectionStatus, "Palette/Colors selection unavailable until API-backed key records are available."); + setText(elements.storageStatus, "Storage import unavailable because the Sprites API is not responding."); ++ setText(elements.filterStatus, "Filters unavailable until Sprites records load from the API."); + setText(elements.replaceStatus, "Replace metadata unavailable until the Sprites API responds."); + renderPreviewPanel(null); + setText(elements.updated, new Date().toLocaleTimeString()); +@@ -316,13 +414,13 @@ function selectSprite(sprite) { + renderPreviewPanel(sprite); } - function renderRows(sprites) { -@@ -292,6 +392,7 @@ function createSpriteRow(sprite) { - actions.className = "action-group action-group--tight"; - actions.append( - createButton("Edit", "spritesEdit", sprite?.key || "", { label: `Edit ${name}` }), -+ createButton("Duplicate", "spritesDuplicateRow", sprite?.key || "", { label: `Duplicate ${name}` }), - createButton(archived ? "Archived" : "Archive", "spritesArchive", sprite?.key || "", { - disabled: archived, - label: archived ? `${name} is already archived` : `Archive ${name}`, -@@ -315,10 +416,7 @@ function createSpriteRow(sprite) { - actionsCell - ); - row.addEventListener("click", () => { -- const key = normalizeText(sprite?.key, "Unavailable"); -- const mimeType = normalizeText(sprite?.mimeType ?? sprite?.mime_type, "Unavailable"); -- const sizeBytes = normalizeText(sprite?.sizeBytes ?? sprite?.size_bytes, "Unavailable"); -- setText(elements.metadata, `${normalizeText(sprite?.name)} (${key}) | ${mimeType} | ${formatDimensions(sprite)} | ${sizeBytes} bytes`); -+ selectSprite(sprite); - }); - return row; - } -@@ -336,12 +434,32 @@ function renderSprites(payload) { +-function renderRows(sprites) { ++function renderRows(sprites, emptyMessage = "No Sprites records returned by the API.") { + if (!elements.tableBody) { + return; + } + if (sprites.length === 0) { + const row = document.createElement("tr"); +- const cell = createCell("No Sprites records returned by the API."); ++ const cell = createCell(emptyMessage); + cell.colSpan = 9; + row.append(cell); + elements.tableBody.replaceChildren(...(editingKey === "__new__" ? [createEditRow(), row] : [row])); +@@ -425,11 +523,13 @@ function renderSprites(payload) { + const sprites = spriteRowsFromPayload(payload); + currentSprites = sprites; + const count = sprites.length; ++ renderFilterOptions(sprites); ++ const visibleSprites = filteredSprites(); + setText(elements.apiStatus, "Ready"); + setText(elements.libraryStatus, count > 0 ? "Ready" : "Empty"); + setText(elements.count, String(count)); + setText(elements.outputStatus, count > 0 ? "Ready" : "Empty"); +- setText(elements.outputSummary, count > 0 ? `${count} sprite record${count === 1 ? "" : "s"} loaded from the API.` : "Sprites API responded with no records."); ++ setText(elements.outputSummary, count > 0 ? `${visibleSprites.length} of ${count} sprite record${count === 1 ? "" : "s"} displayed from the API.` : "Sprites API responded with no records."); + setText(elements.emptyState, count > 0 ? "" : "No Sprites records returned by the API."); setText(elements.updated, new Date().toLocaleTimeString()); setText(elements.metadata, count > 0 ? "Select a sprite row to review its metadata." : "No sprite metadata available yet."); - setActionStatus("Ready for API-backed edits."); -+ setText(elements.storageStatus, "Binary upload/storage import is not configured for Sprites yet. Existing source and storage metadata can be reviewed and replaced through the API."); -+ setText(elements.replaceStatus, "Select a sprite to replace source metadata or duplicate with a server-owned key."); -+ selectSprite(sprites.find((sprite) => sprite.key === selectedSpriteKey) || null); +@@ -440,7 +540,11 @@ function renderSprites(payload) { setHidden(elements.emptyState, count > 0); setHidden(elements.errorState, true); renderPaletteStatus(sprites); - renderRows(sprites); +- renderRows(sprites); ++ renderFilterStatus(visibleSprites.length, count); ++ renderRows( ++ visibleSprites, ++ count > 0 ? "No Sprites records match current filters." : "No Sprites records returned by the API." ++ ); } -+function bodyFromSprite(sprite, overrides = {}) { -+ return { -+ category: normalizeCategory(overrides.category ?? sprite?.category), -+ height: sprite?.height ?? null, -+ mimeType: String(overrides.mimeType ?? sprite?.mimeType ?? "").trim(), -+ name: String(overrides.name ?? sprite?.name ?? "").trim(), -+ originalName: String(overrides.originalName ?? sprite?.originalName ?? "").trim(), -+ paletteColorKeys: paletteKeysFor(sprite), -+ sizeBytes: sprite?.sizeBytes ?? null, -+ source: String(overrides.source ?? sprite?.source ?? "").trim(), -+ status: String(overrides.status ?? sprite?.status ?? "").trim(), -+ storagePath: String(overrides.storagePath ?? sprite?.storagePath ?? "").trim(), -+ tagKeys: Array.isArray(sprite?.tagKeys) ? sprite.tagKeys : [], -+ width: sprite?.width ?? null, -+ }; -+} -+ - function collectEditingValues(row) { - return { - category: normalizeCategory(row.querySelector("[data-sprites-category-input]")?.value), -@@ -456,6 +574,50 @@ async function deleteSprite(key) { - } - } + function bodyFromSprite(sprite, overrides = {}) { +@@ -648,7 +752,7 @@ elements.refresh?.addEventListener("click", () => { -+async function duplicateSprite(key) { -+ const sprite = currentSprites.find((item) => item.key === key); -+ if (!sprite) { -+ setActionStatus("Select a sprite before duplicating."); -+ return; -+ } -+ try { -+ setActionStatus("Duplicating sprite through the API."); -+ const payload = await writeSprite(SPRITES_API_PATH, bodyFromSprite(sprite, { -+ name: `${normalizeText(sprite.name, "Sprite")} Copy`, -+ status: sprite.status || "draft", -+ })); -+ if (!payload) { -+ return; -+ } -+ selectedSpriteKey = payload?.data?.sprite?.key || ""; -+ setActionStatus("Sprite duplicated with an API-owned key."); -+ await loadSprites(); -+ } catch (error) { -+ setActionStatus(error instanceof Error ? error.message : "Sprite duplicate failed."); -+ } -+} -+ -+async function replaceSpriteMetadata(key) { -+ const sprite = currentSprites.find((item) => item.key === key); -+ if (!sprite) { -+ setActionStatus("Select a sprite before replacing metadata."); -+ return; -+ } -+ try { -+ setActionStatus("Replacing sprite source metadata through the API."); -+ const payload = await writeSprite(`${SPRITES_API_PATH}/${encodeURIComponent(key)}`, bodyFromSprite(sprite, { -+ source: sprite.source || sprite.storagePath || sprite.sourceName || "", -+ })); -+ if (!payload) { -+ return; -+ } -+ setActionStatus("Sprite source metadata replaced."); -+ await loadSprites(); -+ } catch (error) { -+ setActionStatus(error instanceof Error ? error.message : "Sprite replace metadata failed."); -+ } -+} -+ - async function loadSprites() { - renderLoading(); - try { -@@ -500,6 +662,7 @@ elements.tableBody?.addEventListener("click", (event) => { - const saveKey = target.dataset.spritesSave; - const archiveKey = target.dataset.spritesArchive; - const deleteKey = target.dataset.spritesDelete; -+ const duplicateKey = target.dataset.spritesDuplicateRow; + elements.add?.addEventListener("click", () => { + editingKey = "__new__"; +- renderRows(currentSprites); ++ renderRows(filteredSprites()); + setActionStatus("New sprite row ready. Name and status are required."); + }); + +@@ -665,13 +769,13 @@ elements.tableBody?.addEventListener("click", (event) => { + const duplicateKey = target.dataset.spritesDuplicateRow; if (editKey !== undefined) { editingKey = editKey; - renderRows(currentSprites); -@@ -523,9 +686,21 @@ elements.tableBody?.addEventListener("click", (event) => { - void archiveSprite(archiveKey); +- renderRows(currentSprites); ++ renderRows(filteredSprites()); + setActionStatus("Editing sprite row. Name and status are required."); return; } -+ if (duplicateKey !== undefined) { -+ void duplicateSprite(duplicateKey); -+ return; -+ } - if (deleteKey !== undefined) { - void deleteSprite(deleteKey); + if (cancelKey !== undefined) { + editingKey = ""; +- renderRows(currentSprites); ++ renderRows(filteredSprites()); + setActionStatus("Sprite edit cancelled."); + return; } +@@ -703,4 +807,37 @@ elements.replace?.addEventListener("click", () => { + void replaceSpriteMetadata(selectedSpriteKey); }); -+elements.duplicate?.addEventListener("click", () => { -+ void duplicateSprite(selectedSpriteKey); ++[elements.search, elements.statusFilter, elements.categoryFilter, elements.tagFilter].forEach((control) => { ++ control?.addEventListener("input", () => { ++ editingKey = ""; ++ const visibleSprites = filteredSprites(); ++ renderFilterStatus(visibleSprites.length, currentSprites.length); ++ renderRows(visibleSprites, "No Sprites records match current filters."); ++ }); ++ control?.addEventListener("change", () => { ++ editingKey = ""; ++ const visibleSprites = filteredSprites(); ++ renderFilterStatus(visibleSprites.length, currentSprites.length); ++ renderRows(visibleSprites, "No Sprites records match current filters."); ++ }); +}); + -+elements.replace?.addEventListener("click", () => { -+ void replaceSpriteMetadata(selectedSpriteKey); ++elements.clearFilters?.addEventListener("click", () => { ++ if (elements.search) { ++ elements.search.value = ""; ++ } ++ if (elements.statusFilter) { ++ elements.statusFilter.value = ""; ++ } ++ if (elements.categoryFilter) { ++ elements.categoryFilter.value = ""; ++ } ++ if (elements.tagFilter) { ++ elements.tagFilter.value = ""; ++ } ++ const visibleSprites = filteredSprites(); ++ renderFilterStatus(visibleSprites.length, currentSprites.length); ++ renderRows(visibleSprites); +}); + void loadSprites(); diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt -index b2b44660d..fa5f97754 100644 +index fa5f97754..6c5cebe36 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt @@ -30,12 +30,12 @@ Files with executed line/function counts where available: (76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29 (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 --(96%) assets/toolbox/sprites/js/index.js - executed lines 485/485; executed functions 43/45 ++(96%) assets/toolbox/sprites/js/index.js - executed lines 776/776; executed functions 76/79 (97%) assets/theme-v2/js/toolbox-status-bar.js - executed lines 427/427; executed functions 35/36 -+(97%) assets/toolbox/sprites/js/index.js - executed lines 649/649; executed functions 57/59 +-(97%) assets/toolbox/sprites/js/index.js - executed lines 649/649; executed functions 57/59 Uncovered or low-coverage changed JS files: (100%) none changed - no changed runtime JS files Changed JS files considered: (0%) tests/playwright/tools/SpritesToolShell.spec.mjs - changed JS file not collected as browser runtime coverage --(96%) assets/toolbox/sprites/js/index.js - changed JS file with browser V8 coverage -+(97%) assets/toolbox/sprites/js/index.js - changed JS file with browser V8 coverage +-(97%) assets/toolbox/sprites/js/index.js - changed JS file with browser V8 coverage ++(96%) assets/toolbox/sprites/js/index.js - changed JS file with browser V8 coverage diff --git a/tests/playwright/tools/SpritesToolShell.spec.mjs b/tests/playwright/tools/SpritesToolShell.spec.mjs -index 3b48fb225..55f7b0718 100644 +index 55f7b0718..8f26757ca 100644 --- a/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/tests/playwright/tools/SpritesToolShell.spec.mjs -@@ -253,3 +253,141 @@ test("Sprites shell archives referenced records and deletes only unreferenced re +@@ -391,3 +391,74 @@ test("Sprites shell duplicates with API-owned key and replaces metadata through await server.close(); } }); + -+test("Sprites shell previews metadata and shows storage and Palette unavailable states", async ({ page }) => { ++test("Sprites shell filters API-backed records by search, category, status, and tag key", async ({ page }) => { + const server = await openSpritesPage(page, async (currentPage) => { + await currentPage.route("**/api/sprites/records", async (route) => { + await route.fulfill({ @@ -332,18 +288,33 @@ index 3b48fb225..55f7b0718 100644 + data: { + sprites: [ + { -+ key: "01J1SPRITEMETA0000000000000", -+ name: "Preview Sprite", ++ category: "Characters", ++ key: "01J1FILTERHERO000000000000", ++ name: "Hero Walk", ++ source: "hero-walk.png", + status: "ready", ++ tagKeys: ["hero", "player"], ++ updatedAt: "2026-06-26T13:00:00.000Z", ++ usageCount: 0, ++ }, ++ { ++ category: "Effects", ++ key: "01J1FILTERSPARK00000000000", ++ name: "Spark Burst", ++ source: "spark.png", ++ status: "draft", ++ tagKeys: ["effect"], ++ updatedAt: "2026-06-26T13:05:00.000Z", ++ usageCount: 0, ++ }, ++ { + category: "Characters", -+ source: "assets/theme-v2/images/image-missing.svg", -+ mimeType: "image/svg+xml", -+ width: 64, -+ height: 32, -+ sizeBytes: 4096, -+ updatedAt: "2026-06-26T12:30:00.000Z", -+ updatedBy: "01J1USER0000000000000000000", -+ paletteColorKeys: ["palette_color_preview"], ++ key: "01J1FILTERVILLAIN000000000", ++ name: "Boss Idle", ++ source: "boss-idle.png", ++ status: "published", ++ tagKeys: ["boss"], ++ updatedAt: "2026-06-26T13:10:00.000Z", + usageCount: 0, + }, + ], @@ -357,222 +328,123 @@ index 3b48fb225..55f7b0718 100644 + }); + + try { -+ await page.locator("[data-sprites-row-key='01J1SPRITEMETA0000000000000']").click(); -+ await expect(page.locator("[data-sprites-preview-panel] img")).toHaveAttribute("src", "assets/theme-v2/images/image-missing.svg"); -+ await expect(page.locator("[data-sprites-preview-panel]")).toContainText("image/svg+xml"); -+ await expect(page.locator("[data-sprites-preview-panel]")).toContainText("64 x 32"); -+ await expect(page.locator("[data-sprites-preview-panel]")).toContainText("4096"); -+ await expect(page.locator("[data-sprites-preview-panel]")).toContainText("palette_color_preview"); -+ await expect(page.locator("[data-sprites-storage-status]")).toContainText("Binary upload/storage import is not configured"); -+ await expect(page.locator("[data-sprites-palette-selection-status]")).toContainText("display-only"); -+ } finally { -+ await workspaceV2CoverageReporter.stop(page); -+ await server.close(); -+ } -+}); -+ -+test("Sprites shell duplicates with API-owned key and replaces metadata through API", async ({ page }) => { -+ const posted = []; -+ let sprites = [ -+ { -+ key: "01J1SPRITEDUPE0000000000000", -+ name: "Duplicate Source", -+ status: "ready", -+ category: "Characters", -+ source: "assets/theme-v2/images/image-missing.svg", -+ mimeType: "image/svg+xml", -+ width: 32, -+ height: 32, -+ sizeBytes: 1024, -+ updatedAt: "2026-06-26T12:40:00.000Z", -+ updatedBy: "01J1USER0000000000000000000", -+ paletteColorKeys: ["palette_color_duplicate"], -+ usageCount: 0, -+ }, -+ ]; -+ const server = await openSpritesPage(page, async (currentPage) => { -+ await currentPage.route("**/api/sprites/records**", async (route) => { -+ const request = route.request(); -+ const url = new URL(request.url()); -+ if (request.method() === "POST") { -+ const body = request.postDataJSON() || {}; -+ posted.push({ body, path: url.pathname }); -+ if (url.pathname === "/api/sprites/records") { -+ const sprite = { -+ ...body, -+ key: "01J1SPRITEDUPECOPY000000000", -+ updatedAt: "2026-06-26T12:45:00.000Z", -+ updatedBy: "01J1USER0000000000000000000", -+ usageCount: 0, -+ }; -+ sprites = [...sprites, sprite]; -+ await route.fulfill({ -+ body: JSON.stringify({ data: { sprite }, ok: true }), -+ contentType: "application/json", -+ status: 200, -+ }); -+ return; -+ } -+ sprites = sprites.map((sprite) => ( -+ url.pathname.includes(sprite.key) -+ ? { ...sprite, ...body, updatedAt: "2026-06-26T12:50:00.000Z" } -+ : sprite -+ )); -+ await route.fulfill({ -+ body: JSON.stringify({ data: { sprite: sprites[0] }, ok: true }), -+ contentType: "application/json", -+ status: 200, -+ }); -+ return; -+ } -+ await route.fulfill({ -+ body: JSON.stringify({ data: { sprites }, ok: true }), -+ contentType: "application/json", -+ status: 200, -+ }); -+ }); -+ }); -+ -+ try { -+ await page.locator("[data-sprites-row-key='01J1SPRITEDUPE0000000000000']").click(); -+ await page.getByRole("button", { name: "Duplicate Sprite" }).click(); -+ await expect(page.locator("[data-sprites-table-body]")).toContainText("Duplicate Source Copy"); -+ await page.locator("[data-sprites-row-key='01J1SPRITEDUPE0000000000000']").click(); -+ await page.getByRole("button", { name: "Replace Metadata" }).click(); -+ expect(posted).toEqual(expect.arrayContaining([ -+ expect.objectContaining({ -+ path: "/api/sprites/records", -+ body: expect.objectContaining({ -+ name: "Duplicate Source Copy", -+ paletteColorKeys: ["palette_color_duplicate"], -+ }), -+ }), -+ expect.objectContaining({ -+ path: "/api/sprites/records/01J1SPRITEDUPE0000000000000", -+ body: expect.objectContaining({ -+ name: "Duplicate Source", -+ source: "assets/theme-v2/images/image-missing.svg", -+ }), -+ }), -+ ])); -+ expect(posted[0].body.key).toBeUndefined(); ++ await expect(page.locator("[data-sprites-filter-status]")).toHaveText("3 Sprites records available."); ++ await page.getByLabel("Search Sprites").fill("hero"); ++ await expect(page.locator("[data-sprites-table-body]")).toContainText("Hero Walk"); ++ await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Spark Burst"); ++ await expect(page.locator("[data-sprites-filter-status]")).toHaveText("1 of 3 Sprites records match current filters."); ++ await page.getByRole("button", { name: "Clear Filters" }).click(); ++ await page.getByLabel("Filter Sprites by category").selectOption("Characters"); ++ await expect(page.locator("[data-sprites-table-body]")).toContainText("Hero Walk"); ++ await expect(page.locator("[data-sprites-table-body]")).toContainText("Boss Idle"); ++ await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Spark Burst"); ++ await page.getByLabel("Filter Sprites by status").selectOption("published"); ++ await expect(page.locator("[data-sprites-table-body]")).toContainText("Boss Idle"); ++ await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Hero Walk"); ++ await page.getByRole("button", { name: "Clear Filters" }).click(); ++ await page.getByLabel("Filter Sprites by tag key").selectOption("effect"); ++ await expect(page.locator("[data-sprites-table-body]")).toContainText("Spark Burst"); ++ await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Hero Walk"); + } finally { + await workspaceV2CoverageReporter.stop(page); + await server.close(); + } +}); diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html -index efab72661..e72610c6b 100644 +index e72610c6b..963582677 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html -@@ -41,6 +41,7 @@ -
-

Reusable colors stay owned by Palette/Colors.

-

Palette/Colors references load with Sprites records when available.

-+

Palette/Colors selection loads when the Palette/Colors API is available.

+@@ -44,6 +44,21 @@ +

Palette/Colors selection loads when the Palette/Colors API is available.

- -@@ -99,6 +100,20 @@ - Selection -
-

Select a sprite row to review its metadata.

-+
-+

No sprite selected for preview.

-+
-+
-+ -+ -+
-+
-+ +
-+ Import ++ Search & Filters +
-+

Storage import is checking API capabilities.

-+

Select a sprite to update source metadata through the API.

-
-
-
-diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-branch-validation.md ++ ++ ++ ++ ++ ++ ++ ++ ++ ++

Filters load with API-backed Sprites records.

++ ++
+ + +
+diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md new file mode 100644 -index 000000000..f9063cc91 +index 000000000..3b1d50f6e --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-branch-validation.md -@@ -0,0 +1,16 @@ -+# PR_26177_CHARLIE_013 Branch Validation ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md +@@ -0,0 +1,12 @@ ++# PR_26177_CHARLIE_014 Branch Validation + +Status: PASS + +## Checks + -+- PASS: PR013 was created as a stacked branch from `PR_26177_CHARLIE_012-sprites-library-crud`. -+- PASS: Stacking is required because preview/metadata/duplicate/replace controls build on the PR012 library CRUD shell. -+- PASS: Current work branch is `PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette`. -+- PASS: Branch contains only the Sprites import/preview/metadata/Palette PR scope relative to PR012. ++- PASS: PR014 was created as a stacked branch from `PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette`. ++- PASS: Stacking is required because search/filter controls build on the PR013 Sprites table and metadata shell. ++- PASS: Current work branch is `PR_26177_CHARLIE_014-sprites-tags-categories-search`. ++- PASS: Branch contains only the Sprites tags/categories/search PR scope relative to PR013. +- PASS: No merge was performed. +- PASS: No `start_of_day` path is changed. -+ -+## Notes -+ -+The current API supports metadata/source/storage reference fields but does not provide binary upload/storage object creation. This PR therefore exposes storage import as unavailable and documents the follow-up instead of adding fake upload behavior. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-manual-validation-notes.md +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md new file mode 100644 -index 000000000..0f4513909 +index 000000000..fbd30a43f --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-manual-validation-notes.md -@@ -0,0 +1,18 @@ -+# PR_26177_CHARLIE_013 Manual Validation Notes ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md +@@ -0,0 +1,13 @@ ++# PR_26177_CHARLIE_014 Manual Validation Notes + +Status: PASS + +## Manual Review + -+- Verified selected sprite metadata is displayed from API response fields only. -+- Verified preview image uses an API-provided local source path when present. -+- Verified missing preview source displays an unavailable state. -+- Verified duplicate omits browser-generated keys and posts to the create API. -+- Verified replace metadata posts to the update API for the selected sprite key. -+- Verified Palette/Colors keys are displayed as references only. -+- Verified Palette/Colors selection is visibly unavailable until an API-backed selector exists. -+- Verified binary upload/storage import is visibly unavailable rather than simulated. -+ -+## Follow-Up -+ -+Add real binary upload/storage import only when a storage API contract exists for Sprites. Add Palette/Colors selector only when Palette/Colors exposes an API-backed key selection contract. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-requirements-checklist.md ++- Verified Search filters by API-returned sprite fields and tag/palette key text. ++- Verified category options are derived from current API records. ++- Verified tag key options are derived from current API records. ++- Verified status filtering uses the Sprites status contract. ++- Verified clear filters restores the unfiltered API-backed table. ++- Verified no static category or tag product-data list was introduced. ++- Verified Sprites does not duplicate Tags ownership. +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md new file mode 100644 -index 000000000..1eafb6af2 +index 000000000..aa85dda6e --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-requirements-checklist.md -@@ -0,0 +1,20 @@ -+# PR_26177_CHARLIE_013 Requirements Checklist ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md +@@ -0,0 +1,19 @@ ++# PR_26177_CHARLIE_014 Requirements Checklist + +Status: PASS + -+- PASS: Evaluated import/upload workflow against current storage/API support. -+- PASS: Did not add fake upload behavior because binary storage import is not available in the current Sprites API contract. -+- PASS: Added visible storage import unavailable state. -+- PASS: Added preview panel. -+- PASS: Added metadata display for image/source name, MIME/type, dimensions, file size, updatedAt, and updatedBy. -+- PASS: Added replace sprite metadata action through the API. -+- PASS: Added duplicate sprite action through the API with server-owned new key. -+- PASS: Displayed Palette/Colors references only as API/database keys. -+- PASS: Displayed Palette/Colors selection unavailable state because selection integration is incomplete. -+- PASS: Did not add Sprite-owned color definitions. -+- PASS: Did not add page-local Palette/Colors arrays. ++- PASS: Added search for Sprites. ++- PASS: Added status filter. ++- PASS: Added categories filter. ++- PASS: Added tag key filter. ++- PASS: Search and filters use API/database-backed sprite data. ++- PASS: Categories are derived from API-backed records. ++- PASS: Tag keys are derived from API-backed records. ++- PASS: Did not create Sprite-owned Tags data. ++- PASS: Did not use page-local product arrays for categories or tags. ++- PASS: Added table filtering UX consistent with GFS patterns. +- PASS: Did not add browser storage product-data source of truth. +- PASS: Did not introduce MEM DB, local-mem, fake-login, or silent fallback. +- PASS: Targeted Playwright coverage passed. +- PASS: Required report artifacts were created. +- PASS: Repo-structured ZIP artifact was created under `tmp/`. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-validation-lane.md +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md new file mode 100644 -index 000000000..17a048046 +index 000000000..f1c57be49 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-validation-lane.md ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md @@ -0,0 +1,33 @@ -+# PR_26177_CHARLIE_013 Validation Lane ++# PR_26177_CHARLIE_014 Validation Lane + +Status: PASS + @@ -600,18 +472,18 @@ index 000000000..17a048046 +node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list +``` + -+Result: PASS, 8 passed. ++Result: PASS, 9 passed. + +## Playwright Coverage + +Targeted Playwright coverage updated `docs_build/dev/reports/playwright_v8_coverage_report.txt` for the Sprites browser module. -diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette.md b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette.md +diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md new file mode 100644 -index 000000000..3721c6709 +index 000000000..fb4161770 --- /dev/null -+++ b/docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette.md ++++ b/docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md @@ -0,0 +1,45 @@ -+# PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette ++# PR_26177_CHARLIE_014-sprites-tags-categories-search + +Team: Charlie + @@ -619,7 +491,7 @@ index 000000000..3721c6709 + +## Scope + -+Added Sprites preview, metadata, duplicate, replace-metadata, and Palette/Colors reference states without introducing fake storage or browser-owned product data. ++Added API-backed search and filters for Sprites records. Categories and tag keys are derived from the API response; Sprites does not create a separate tag authority or page-local product data list. + +## Changed Files + @@ -629,21 +501,21 @@ index 000000000..3721c6709 +- `docs_build/dev/reports/playwright_v8_coverage_report.txt` +- `docs_build/dev/reports/codex_review.diff` +- `docs_build/dev/reports/codex_changed_files.txt` -+- `docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette.md` -+- `docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-branch-validation.md` -+- `docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-requirements-checklist.md` -+- `docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-validation-lane.md` -+- `docs_build/dev/reports/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette-manual-validation-notes.md` ++- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search.md` ++- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-branch-validation.md` ++- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-requirements-checklist.md` ++- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-validation-lane.md` ++- `docs_build/dev/reports/PR_26177_CHARLIE_014-sprites-tags-categories-search-manual-validation-notes.md` + +## Implementation Notes + -+- Added selected sprite preview panel. -+- Added metadata display for file/source, MIME/type, dimensions, file size, updatedAt, updatedBy, and Palette/Colors keys. -+- Added duplicate action through the Sprites create API so the server owns the new key. -+- Added replace-metadata action through the Sprites update API. -+- Added visible storage import unavailable state because the current Sprites API contract does not provide binary upload/storage write support. -+- Added Palette/Colors selection unavailable/display-only state because Palette/Colors key selection integration is not present in the current contract. -+- No reusable color definitions were added to Sprites. ++- Added search control. ++- Added status filter using the Sprites API status contract. ++- Added category filter derived from API-backed sprite records. ++- Added tag key filter derived from API-backed sprite records. ++- Added clear filters action. ++- Added filter status summary for visible records versus total API-backed records. ++- Did not duplicate Tags ownership or add page-local reusable product arrays. + +## Validation + @@ -655,4 +527,4 @@ index 000000000..3721c6709 + +## ZIP Artifact + -+- `tmp/PR_26177_CHARLIE_013-sprites-import-preview-metadata-palette_delta.zip` ++- `tmp/PR_26177_CHARLIE_014-sprites-tags-categories-search_delta.zip` diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt index fa5f97754..6c5cebe36 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt @@ -30,12 +30,12 @@ Files with executed line/function counts where available: (76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29 (77%) assets/theme-v2/js/tool-display-mode.js - executed lines 304/304; executed functions 23/30 (80%) assets/theme-v2/js/theme-icons.js - executed lines 69/69; executed functions 4/5 +(96%) assets/toolbox/sprites/js/index.js - executed lines 776/776; executed functions 76/79 (97%) assets/theme-v2/js/toolbox-status-bar.js - executed lines 427/427; executed functions 35/36 -(97%) assets/toolbox/sprites/js/index.js - executed lines 649/649; executed functions 57/59 Uncovered or low-coverage changed JS files: (100%) none changed - no changed runtime JS files Changed JS files considered: (0%) tests/playwright/tools/SpritesToolShell.spec.mjs - changed JS file not collected as browser runtime coverage -(97%) assets/toolbox/sprites/js/index.js - changed JS file with browser V8 coverage +(96%) assets/toolbox/sprites/js/index.js - changed JS file with browser V8 coverage diff --git a/tests/playwright/tools/SpritesToolShell.spec.mjs b/tests/playwright/tools/SpritesToolShell.spec.mjs index 55f7b0718..8f26757ca 100644 --- a/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/tests/playwright/tools/SpritesToolShell.spec.mjs @@ -391,3 +391,74 @@ test("Sprites shell duplicates with API-owned key and replaces metadata through await server.close(); } }); + +test("Sprites shell filters API-backed records by search, category, status, and tag key", async ({ page }) => { + const server = await openSpritesPage(page, async (currentPage) => { + await currentPage.route("**/api/sprites/records", async (route) => { + await route.fulfill({ + body: JSON.stringify({ + data: { + sprites: [ + { + category: "Characters", + key: "01J1FILTERHERO000000000000", + name: "Hero Walk", + source: "hero-walk.png", + status: "ready", + tagKeys: ["hero", "player"], + updatedAt: "2026-06-26T13:00:00.000Z", + usageCount: 0, + }, + { + category: "Effects", + key: "01J1FILTERSPARK00000000000", + name: "Spark Burst", + source: "spark.png", + status: "draft", + tagKeys: ["effect"], + updatedAt: "2026-06-26T13:05:00.000Z", + usageCount: 0, + }, + { + category: "Characters", + key: "01J1FILTERVILLAIN000000000", + name: "Boss Idle", + source: "boss-idle.png", + status: "published", + tagKeys: ["boss"], + updatedAt: "2026-06-26T13:10:00.000Z", + usageCount: 0, + }, + ], + }, + ok: true, + }), + contentType: "application/json", + status: 200, + }); + }); + }); + + try { + await expect(page.locator("[data-sprites-filter-status]")).toHaveText("3 Sprites records available."); + await page.getByLabel("Search Sprites").fill("hero"); + await expect(page.locator("[data-sprites-table-body]")).toContainText("Hero Walk"); + await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Spark Burst"); + await expect(page.locator("[data-sprites-filter-status]")).toHaveText("1 of 3 Sprites records match current filters."); + await page.getByRole("button", { name: "Clear Filters" }).click(); + await page.getByLabel("Filter Sprites by category").selectOption("Characters"); + await expect(page.locator("[data-sprites-table-body]")).toContainText("Hero Walk"); + await expect(page.locator("[data-sprites-table-body]")).toContainText("Boss Idle"); + await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Spark Burst"); + await page.getByLabel("Filter Sprites by status").selectOption("published"); + await expect(page.locator("[data-sprites-table-body]")).toContainText("Boss Idle"); + await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Hero Walk"); + await page.getByRole("button", { name: "Clear Filters" }).click(); + await page.getByLabel("Filter Sprites by tag key").selectOption("effect"); + await expect(page.locator("[data-sprites-table-body]")).toContainText("Spark Burst"); + await expect(page.locator("[data-sprites-table-body]")).not.toContainText("Hero Walk"); + } finally { + await workspaceV2CoverageReporter.stop(page); + await server.close(); + } +}); diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html index e72610c6b..963582677 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html @@ -44,6 +44,21 @@

Sprites

Palette/Colors selection loads when the Palette/Colors API is available.

+
+ Search & Filters +
+ + + + + + + + + +

Filters load with API-backed Sprites records.

+
+