diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js new file mode 100644 index 000000000..3e5f8299b --- /dev/null +++ b/assets/toolbox/sprites/js/index.js @@ -0,0 +1,503 @@ +import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js"; +import { getSessionCurrent } from "../../../../src/api/session-api-client.js"; + +const repository = createServerRepositoryClient("sprites"); + +const elements = { + add: document.querySelector("[data-sprites-add]"), + categoryFilter: document.querySelector("[data-sprites-category-filter]"), + count: document.querySelector("[data-sprites-count]"), + libraryStatus: document.querySelector("[data-sprites-library-status]"), + log: document.querySelector("[data-sprites-log]"), + metadata: document.querySelector("[data-sprites-metadata]"), + paletteState: document.querySelector("[data-sprites-palette-state]"), + preview: document.querySelector("[data-sprites-preview]"), + references: document.querySelector("[data-sprites-references]"), + search: document.querySelector("[data-sprites-search]"), + statusFilter: document.querySelector("[data-sprites-status-filter]"), + tableBody: document.querySelector("[data-sprites-table-body]"), + tagFilter: document.querySelector("[data-sprites-tag-filter]"), + validation: document.querySelector("[data-sprites-validation]"), + visibleCount: document.querySelector("[data-sprites-visible-count]"), +}; + +let snapshot = emptySnapshot(); +let editingSpriteId = ""; +let selectedSpriteId = ""; +let filterState = { + category: "", + query: "", + status: "", + tagKey: "", +}; + +function emptySnapshot() { + return { + categories: [], + paletteColors: [], + paletteOwnership: "Palette/Colors is the reusable color source of truth.", + preview: { message: "Preview unavailable until the Sprites API is ready.", status: "Unavailable" }, + referenceContract: { message: "Reference contract unavailable.", references: [], usageCount: 0 }, + selectedSprite: null, + sprites: [], + statusOptions: [], + tags: [], + validation: { findings: [], status: "Loading" }, + }; +} + +function normalizeText(value) { + return String(value || "").trim(); +} + +function setText(target, value) { + if (target) { + target.textContent = value; + } +} + +function option(value, label = value) { + const optionElement = document.createElement("option"); + optionElement.value = value; + optionElement.textContent = label; + return optionElement; +} + +function replaceSelectOptions(select, values, selectedValue = "", includeAllLabel = "") { + if (!select) { + return; + } + const options = includeAllLabel ? [option("", includeAllLabel)] : []; + values.forEach((value) => { + options.push(option(value)); + }); + select.replaceChildren(...options); + select.value = selectedValue; +} + +function tagLabel(tagKey) { + return snapshot.tags.find((tag) => tag.id === tagKey)?.name || tagKey; +} + +function paletteLabel(paletteColorKey) { + const swatch = snapshot.paletteColors.find((color) => color.key === paletteColorKey); + return swatch ? `${swatch.name} (${swatch.key})` : paletteColorKey || "None"; +} + +function currentSession() { + try { + return getSessionCurrent(); + } catch { + return { authenticated: false }; + } +} + +function requireCreatorSession() { + if (currentSession()?.authenticated === true) { + return true; + } + window.location.href = "account/sign-in.html"; + return false; +} + +function loadSnapshot() { + const result = repository.getSpritesSnapshot(); + if (!result || result.error) { + const diagnostic = result?.message || result?.validation?.findings?.[0]?.action || repository.__apiDiagnostic?.() || "Sprites API unavailable."; + snapshot = emptySnapshot(); + snapshot.validation.findings = [{ action: diagnostic, label: "Sprites API", status: "Blocked" }]; + snapshot.validation.status = "Blocked"; + setText(elements.log, diagnostic); + render(); + return; + } + snapshot = result; + if (!selectedSpriteId && snapshot.selectedSprite?.id) { + selectedSpriteId = snapshot.selectedSprite.id; + } + if (selectedSpriteId && !snapshot.sprites.some((sprite) => sprite.id === selectedSpriteId)) { + selectedSpriteId = snapshot.selectedSprite?.id || snapshot.sprites[0]?.id || ""; + } + render(); +} + +function filteredSprites() { + const query = filterState.query.toLowerCase(); + return snapshot.sprites.filter((sprite) => { + const matchesStatus = !filterState.status || sprite.status === filterState.status; + const matchesCategory = !filterState.category || sprite.category === filterState.category; + const matchesTag = !filterState.tagKey || (Array.isArray(sprite.tagKeys) && sprite.tagKeys.includes(filterState.tagKey)); + const searchable = [ + sprite.name, + sprite.key, + sprite.category, + sprite.status, + sprite.paletteColorKey, + ...(Array.isArray(sprite.tagKeys) ? sprite.tagKeys.map(tagLabel) : []), + ].join(" ").toLowerCase(); + const matchesQuery = !query || searchable.includes(query); + return matchesStatus && matchesCategory && matchesTag && matchesQuery; + }); +} + +function createCell(text) { + const cell = document.createElement("td"); + cell.textContent = text; + return cell; +} + +function createButton(label, action, spriteId = "") { + const button = document.createElement("button"); + button.className = "btn btn--compact"; + button.type = "button"; + button.dataset.spritesAction = action; + if (spriteId) { + button.dataset.spriteId = spriteId; + } + button.textContent = label; + return button; +} + +function createInput(value, label, name) { + const input = document.createElement("input"); + input.type = "text"; + input.value = value || ""; + input.setAttribute("aria-label", label); + input.dataset.spritesField = name; + return input; +} + +function createSelect(values, selectedValue, label, name, emptyLabel = "") { + const select = document.createElement("select"); + select.setAttribute("aria-label", label); + select.dataset.spritesField = name; + const options = []; + if (emptyLabel) { + options.push(option("", emptyLabel)); + } + values.forEach((value) => options.push(option(value))); + select.replaceChildren(...options); + select.value = selectedValue || ""; + return select; +} + +function createPaletteSelect(selectedValue) { + const select = document.createElement("select"); + select.setAttribute("aria-label", "Palette color reference"); + select.dataset.spritesField = "paletteColorKey"; + const options = [option("", "No Palette color reference")]; + snapshot.paletteColors.forEach((color) => { + options.push(option(color.key, `${color.name} (${color.key})`)); + }); + select.replaceChildren(...options); + select.value = selectedValue || ""; + if (snapshot.paletteColors.length === 0) { + select.disabled = true; + } + return select; +} + +function createTagSelect(selectedTagKeys = []) { + const select = document.createElement("select"); + select.setAttribute("aria-label", "Shared tag reference"); + select.dataset.spritesField = "tagKeys"; + select.replaceChildren( + option("", "No shared tag"), + ...snapshot.tags.map((tag) => option(tag.id, tag.name)), + ); + select.value = selectedTagKeys[0] || ""; + if (snapshot.tags.length === 0) { + select.disabled = true; + } + return select; +} + +function createEditorRow(sprite = {}) { + const row = document.createElement("tr"); + const isNew = !sprite.id; + row.dataset.spritesEditorRow = isNew ? "__new__" : sprite.id; + + const nameCell = document.createElement("td"); + nameCell.append(createInput(sprite.name || "", "Sprite name", "name")); + row.append(nameCell); + + const statusCell = document.createElement("td"); + statusCell.append(createSelect(snapshot.statusOptions, sprite.status || "Ready", "Sprite status", "status")); + row.append(statusCell); + + const categoryCell = document.createElement("td"); + categoryCell.append(createSelect(snapshot.categories, sprite.category || "Sprite", "Sprite category", "category")); + row.append(categoryCell); + + const tagsCell = document.createElement("td"); + tagsCell.append(createTagSelect(sprite.tagKeys || [])); + row.append(tagsCell); + + const paletteCell = document.createElement("td"); + paletteCell.append(createPaletteSelect(sprite.paletteColorKey || "")); + row.append(paletteCell); + + row.append(createCell(isNew ? "New sprite" : sprite.updatedAt || "Unavailable")); + + const actionsCell = document.createElement("td"); + actionsCell.append( + createButton("Save", "save", sprite.id || "__new__"), + document.createTextNode(" "), + createButton("Cancel", "cancel", sprite.id || "__new__"), + ); + row.append(actionsCell); + return row; +} + +function rowValues(row) { + const field = (name) => row.querySelector(`[data-sprites-field='${name}']`); + const tagKey = normalizeText(field("tagKeys")?.value); + return { + category: normalizeText(field("category")?.value), + name: normalizeText(field("name")?.value), + paletteColorKey: normalizeText(field("paletteColorKey")?.value), + status: normalizeText(field("status")?.value), + tagKeys: tagKey ? [tagKey] : [], + }; +} + +function renderRows() { + if (!elements.tableBody) { + return; + } + const rows = []; + const sprites = filteredSprites(); + + sprites.forEach((sprite) => { + if (editingSpriteId === sprite.id) { + rows.push(createEditorRow(sprite)); + return; + } + const row = document.createElement("tr"); + row.dataset.spritesRow = sprite.id; + if (sprite.id === selectedSpriteId) { + row.dataset.spritesSelected = "true"; + } + row.append( + createCell(sprite.name || "Unnamed Sprite"), + createCell(sprite.status || "Unavailable"), + createCell(sprite.category || "Uncategorized"), + createCell((sprite.tagKeys || []).map(tagLabel).join(", ") || "No shared tags"), + createCell(sprite.paletteColorKey || "None"), + createCell(sprite.updatedAt || "Unavailable"), + ); + const actionsCell = document.createElement("td"); + actionsCell.append( + createButton("View", "view", sprite.id), + document.createTextNode(" "), + createButton("Edit", "edit", sprite.id), + document.createTextNode(" "), + createButton("Archive", "archive", sprite.id), + ); + row.append(actionsCell); + rows.push(row); + }); + + if (editingSpriteId === "__new__") { + rows.push(createEditorRow()); + } + + if (rows.length === 0) { + const emptyRow = document.createElement("tr"); + const emptyCell = document.createElement("td"); + emptyCell.colSpan = 7; + emptyCell.textContent = snapshot.sprites.length === 0 + ? "No sprite records yet. Add a sprite to begin testing the MVP." + : "No sprites match the current filters."; + emptyRow.append(emptyCell); + rows.push(emptyRow); + } + elements.tableBody.replaceChildren(...rows); +} + +function renderFilters() { + replaceSelectOptions(elements.statusFilter, snapshot.statusOptions, filterState.status, "All statuses"); + replaceSelectOptions(elements.categoryFilter, snapshot.categories, filterState.category, "All categories"); + if (elements.tagFilter) { + const tagOptions = [option("", "All tags"), ...snapshot.tags.map((tag) => option(tag.id, tag.name))]; + elements.tagFilter.replaceChildren(...tagOptions); + elements.tagFilter.value = filterState.tagKey; + } +} + +function renderPaletteState() { + if (!elements.paletteState) { + return; + } + if (snapshot.paletteColors.length === 0) { + elements.paletteState.textContent = "Palette/Colors has no reusable color records available. Sprites will save Palette color references as empty keys."; + return; + } + elements.paletteState.textContent = `${snapshot.paletteColors.length} Palette/Colors record${snapshot.paletteColors.length === 1 ? "" : "s"} available by API key. Sprites store paletteColorKey only.`; +} + +function renderMetadata() { + const sprite = snapshot.sprites.find((candidate) => candidate.id === selectedSpriteId) || snapshot.selectedSprite; + if (!elements.metadata || !elements.preview || !elements.references) { + return; + } + if (!sprite) { + elements.metadata.replaceChildren(listItem("No selected sprite details.")); + elements.preview.replaceChildren(paragraph(snapshot.preview.message || "Preview unavailable.")); + elements.references.replaceChildren(paragraph(snapshot.referenceContract.message || "Reference protection unavailable.")); + return; + } + elements.metadata.replaceChildren( + listItem(`Key: ${sprite.key || sprite.id}`), + listItem(`Source: ${sprite.source || "Unavailable"}`), + listItem(`File: ${sprite.fileName || sprite.originalName || "Unavailable"}`), + listItem(`MIME/type: ${sprite.mimeType || "Unavailable"}`), + listItem(`Dimensions: ${sprite.dimensions || "Unavailable"}`), + listItem(`Size: ${sprite.size ? `${sprite.size} bytes` : "Unavailable"}`), + listItem(`Palette color key: ${sprite.paletteColorKey || "None"}`), + listItem(`Updated by: ${sprite.updatedBy || "Unavailable"}`), + listItem(`Updated at: ${sprite.updatedAt || "Unavailable"}`), + ); + elements.preview.replaceChildren( + paragraph(sprite.previewStatus || snapshot.preview.message || "Preview unavailable."), + paragraph(`Stored path: ${sprite.storedPath || sprite.path || "Unavailable"}`), + ); + const referenceSummary = sprite.referenceSummary || snapshot.referenceContract; + elements.references.replaceChildren( + paragraph(referenceSummary.message || "Reference protection unavailable."), + paragraph(`Usage count: ${referenceSummary.usageCount || 0}`), + ); +} + +function listItem(text) { + const item = document.createElement("li"); + item.textContent = text; + return item; +} + +function paragraph(text) { + const item = document.createElement("p"); + item.textContent = text; + return item; +} + +function renderValidation() { + if (!elements.validation) { + return; + } + const findings = snapshot.validation?.findings || []; + if (findings.length === 0) { + elements.validation.replaceChildren(listItem("No validation findings.")); + return; + } + elements.validation.replaceChildren( + ...findings.map((finding) => listItem(`${finding.label || "Finding"}: ${finding.action || finding.status || "Needs input"}`)), + ); +} + +function renderSummary() { + const visible = filteredSprites(); + setText(elements.count, String(snapshot.sprites.length)); + setText(elements.visibleCount, String(visible.length)); + setText(elements.libraryStatus, snapshot.validation?.status || "Unavailable"); + if (!elements.log?.textContent || elements.log.textContent === "Sprites loading.") { + setText(elements.log, snapshot.sprites.length ? "Sprites ready." : "Sprites ready. Add a sprite to begin."); + } +} + +function render() { + renderFilters(); + renderPaletteState(); + renderRows(); + renderMetadata(); + renderValidation(); + renderSummary(); +} + +function saveEditorRow(row) { + if (!requireCreatorSession()) { + return; + } + const spriteId = row.dataset.spritesEditorRow; + const values = rowValues(row); + const result = spriteId === "__new__" + ? repository.createSpriteRecord(values) + : repository.updateSpriteRecord(spriteId, values); + setText(elements.log, result?.message || "Sprite save did not return a message."); + if (result?.created || result?.updated) { + editingSpriteId = ""; + selectedSpriteId = result.asset?.id || selectedSpriteId; + } + loadSnapshot(); +} + +elements.add?.addEventListener("click", () => { + editingSpriteId = "__new__"; + selectedSpriteId = ""; + setText(elements.log, "Adding sprite."); + render(); +}); + +elements.tableBody?.addEventListener("click", (event) => { + const button = event.target.closest("[data-sprites-action]"); + if (!button) { + return; + } + const spriteId = button.dataset.spriteId || ""; + const action = button.dataset.spritesAction; + if (action === "view") { + selectedSpriteId = spriteId; + setText(elements.log, "Viewing sprite."); + render(); + return; + } + if (action === "edit") { + selectedSpriteId = spriteId; + editingSpriteId = spriteId; + setText(elements.log, "Editing sprite."); + render(); + return; + } + if (action === "cancel") { + editingSpriteId = ""; + setText(elements.log, "Sprite edit canceled."); + render(); + return; + } + if (action === "save") { + const row = button.closest("[data-sprites-editor-row]"); + saveEditorRow(row); + return; + } + if (action === "archive") { + if (!requireCreatorSession()) { + return; + } + const result = repository.archiveSpriteRecord(spriteId); + setText(elements.log, result?.message || "Sprite archive did not return a message."); + selectedSpriteId = spriteId; + loadSnapshot(); + } +}); + +elements.search?.addEventListener("input", () => { + filterState.query = normalizeText(elements.search.value); + render(); +}); + +elements.statusFilter?.addEventListener("change", () => { + filterState.status = normalizeText(elements.statusFilter.value); + render(); +}); + +elements.categoryFilter?.addEventListener("change", () => { + filterState.category = normalizeText(elements.categoryFilter.value); + render(); +}); + +elements.tagFilter?.addEventListener("change", () => { + filterState.tagKey = normalizeText(elements.tagFilter.value); + render(); +}); + +loadSnapshot(); diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion.md b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion.md new file mode 100644 index 000000000..fd2e7ba7d --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion.md @@ -0,0 +1,56 @@ +# PR_26177_CHARLIE_018-sprites-testable-mvp-completion + +Team: Charlie + +Status: PASS + +## Scope + +Completed the Sprites MVP to a manually testable state without splitting additional PRs. + +Implemented: +- Toolbox Sprites entry is active/clickable through the source-controlled registry. +- `/toolbox/sprites/index.html` loads a Theme V2 Sprites workspace. +- Removed placeholder-era visible wording including `Not implemented yet.`, `Setup`, `Plan sprite creation`, `future rebuild work`, and placeholder Workspace/Inspector/Output sections. +- Sprites uses Web UI -> API/service contract -> asset repository. +- Sprite list/create/edit/archive flows are API-backed through `/api/toolbox/sprites`. +- Guest save attempts redirect to `account/sign-in.html`. +- Preview/metadata surface shows product-safe metadata and explicit unavailable preview state. +- Palette/Colors remains reusable color SSoT; Sprites stores `paletteColorKey` only. +- Search/filter/status/category/tag controls are available. +- Reference protection disables destructive delete and shows explicit unavailable Object/World reference state. + +## Changed Files + +- `assets/toolbox/sprites/js/index.js` +- `docs_build/dev/reports/coverage_changed_js_guardrail.txt` +- `docs_build/dev/reports/playwright_v8_coverage_report.txt` +- `src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js` +- `src/dev-runtime/server/local-api-router.mjs` +- `src/shared/toolbox/tool-metadata-inventory.js` +- `tests/dev-runtime/SpritesAssetRepository.test.mjs` +- `tests/playwright/tools/SpritesToolMvp.spec.mjs` +- `tests/playwright/tools/ToolboxRoutePages.spec.mjs` +- `toolbox/sprites/index.html` + +## Validation + +PASS `node ./scripts/run-node-test-files.mjs tests/dev-runtime/SpritesAssetRepository.test.mjs` + +PASS `npx playwright test tests/playwright/tools/SpritesToolMvp.spec.mjs --workers=1 --reporter=list` + +PASS `npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --workers=1 --reporter=list` + +PASS `git diff --check` + +PASS static scan for inline style/script/event handler usage in Sprites HTML/JS. + +PASS static scan for forbidden Sprites persistence patterns in Sprites HTML/JS. + +## Notes + +- No `start_of_day` files changed. +- No browser storage product-data SSoT introduced. +- No MEM DB, local-mem, fake-login, SQLite direction, or silent product-data fallback introduced. +- Sprites deliberately reuses the existing asset repository through a Sprites API alias rather than adding a parallel database architecture. +- Destructive delete remains disabled until Object/World reference contracts can verify real references. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_branch-validation.md b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_branch-validation.md new file mode 100644 index 000000000..4bdbdd480 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_branch-validation.md @@ -0,0 +1,30 @@ +# Branch Validation + +PR: PR_26177_CHARLIE_018-sprites-testable-mvp-completion + +Status: PASS + +## Branch Gate + +- Current branch: `PR_26177_CHARLIE_018-sprites-testable-mvp-completion` +- Started after synced `main`: PASS +- Worktree before implementation: clean on new PR branch +- Remote upstream: not present until PR018 push + +## Scope Gate + +- No `start_of_day` files changed: PASS +- One PR purpose only: PASS +- No unrelated cleanup: PASS +- Runtime/API/UI changes limited to Sprites MVP testability and affected Toolbox metadata/tests: PASS +- No direct commits to `main`: PASS + +## Changed-File Check + +PASS: changed files are Sprites UI/API contract, affected Toolbox metadata/tests, required reports, and generated validation coverage reports. + +## Final Branch State + +- Branch tracks `origin/PR_26177_CHARLIE_018-sprites-testable-mvp-completion`. +- Branch is updated from synced `main` and remains the active PR018 branch for this validation refresh. +- Commit/push will be refreshed after report and ZIP regeneration. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_manual-validation-notes.md b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_manual-validation-notes.md new file mode 100644 index 000000000..c48a02736 --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_manual-validation-notes.md @@ -0,0 +1,33 @@ +# Manual Validation Notes + +PR: PR_26177_CHARLIE_018-sprites-testable-mvp-completion + +Status: PASS + +## Manual Test Steps + +1. Start the local API/site server. +2. Open `/toolbox/index.html`. +3. Confirm the Sprites card is visible without enabling Planned tools. +4. Click Sprites and confirm navigation to `/toolbox/sprites/index.html`. +5. Confirm the Sprites table, summary cards, filters, preview, metadata, references, validation panels, and `Sprite Details` side panel render. +6. Click `Add Sprite`. +7. Enter `Hero Sprite`. +8. Select category `Character`. +9. Save and confirm a row appears with status `Ready`. +10. Click `Edit`, change the name to `Hero Sprite Revised`, choose category `Icon`, and save. +11. Search for `revised` and confirm only the matching sprite remains visible. +12. Filter category `Icon` and confirm the matching sprite remains visible. +13. Click `Archive` and confirm status changes to `Archived`. +14. Confirm the metadata panel shows key, file/source details, size/dimensions unavailable where appropriate, palette color key, updated by, and updated at. +15. Confirm the reference panel says destructive delete is disabled until Object/World reference contracts are available. +16. Sign out or set a guest session. +17. Open `/toolbox/sprites/index.html`, click `Add Sprite`, enter a name, and save. +18. Confirm the browser redirects to `/account/sign-in.html`. +19. Confirm Palette/Colors references are shown by key only and no Sprites-owned reusable color definitions are present. + +## Manual Notes + +- Preview is intentionally metadata-safe until storage/image byte preview integration is available. +- Palette/Colors empty state is explicit when no reusable color records are available. +- Destructive delete is intentionally unavailable; archive is the supported MVP lifecycle operation. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_requirements-checklist.md b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_requirements-checklist.md new file mode 100644 index 000000000..1c583c7ee --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_requirements-checklist.md @@ -0,0 +1,26 @@ +# Requirement Checklist + +PR: PR_26177_CHARLIE_018-sprites-testable-mvp-completion + +Status: PASS + +- PASS Ensure `/toolbox/index.html` Sprites is active/clickable. +- PASS Ensure `/toolbox/sprites/index.html` loads. +- PASS `/toolbox/sprites/index.html` no longer shows `Not implemented yet.`, `Setup`, `Plan sprite creation`, `future rebuild work`, or placeholder Workspace/Inspector/Output sections. +- PASS Ensure Sprites tool has working table/list surface. +- PASS Ensure API-backed list/create/edit/archive works. +- PASS Ensure guest save redirects to `account/sign-in.html`. +- PASS Ensure preview/metadata surface works or shows explicit product-safe unavailable state. +- PASS Ensure Palette/Colors is the only source for reusable colors. +- PASS Ensure Sprites references Palette/Colors by key only. +- PASS Ensure Sprites does not own color definitions or page-local color arrays. +- PASS Ensure search/filter/tags/categories work where supported. +- PASS Ensure reference viewer/delete protection works or shows clear empty state. +- PASS Remove planned state where it blocks Sprites testing. +- PASS Keep Theme V2 compliance. +- PASS No inline CSS, inline JS, style blocks, script blocks, or inline event handlers in Sprites page. +- PASS Do not modify `start_of_day` folders. +- PASS No unrelated cleanup. +- PASS Do not introduce MEM DB, local-mem, fake-login, browser-owned product data, browser storage product-data SSoT, SQLite direction, or silent fallbacks. +- PASS Maintain Web UI -> API/service contract -> database/repository flow. +- PASS Browser does not generate authoritative database keys. diff --git a/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_validation-lane.md b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_validation-lane.md new file mode 100644 index 000000000..2db6cdd6f --- /dev/null +++ b/docs_build/dev/reports/PR_26177_CHARLIE_018-sprites-testable-mvp-completion_validation-lane.md @@ -0,0 +1,36 @@ +# Validation Lane + +PR: PR_26177_CHARLIE_018-sprites-testable-mvp-completion + +Status: PASS + +## Commands + +```text +node ./scripts/run-node-test-files.mjs tests/dev-runtime/SpritesAssetRepository.test.mjs +PASS tests/dev-runtime/SpritesAssetRepository.test.mjs +1/1 targeted node test file(s) passed. +``` + +```text +npx playwright test tests/playwright/tools/SpritesToolMvp.spec.mjs --workers=1 --reporter=list +3 passed +``` + +```text +npx playwright test tests/playwright/tools/ToolboxRoutePages.spec.mjs --workers=1 --reporter=list +11 passed +``` + +```text +git diff --check +PASS +``` + +## Static Checks + +- PASS Sprites HTML/JS has no inline style/script/event handler matches. +- PASS Sprites HTML/JS has no browser storage product-data SSoT matches. +- PASS `toolbox/sprites/index.html` no longer contains the blocked placeholder wording. +- PASS No `start_of_day` changed files. +- NOTE Broad scan over all changed files finds pre-existing hex literals in `tests/playwright/tools/ToolboxRoutePages.spec.mjs`; Sprites implementation does not introduce reusable color definitions. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 0172ab7b9..183d8af33 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,7 +1,11 @@ -docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership.md -docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_branch-validation.md -docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_validation-checklist.md -docs_build/dev/reports/codex_changed_files.txt -docs_build/dev/reports/codex_review.diff +# git status --short +M docs_build/dev/reports/coverage_changed_js_guardrail.txt + M docs_build/dev/reports/playwright_v8_coverage_report.txt + +# git ls-files --others --exclude-standard +(no output) + +# git diff --stat +docs_build/dev/reports/coverage_changed_js_guardrail.txt | 8 ++------ + docs_build/dev/reports/playwright_v8_coverage_report.txt | 16 +++------------- + 2 files changed, 5 insertions(+), 19 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 be5b1c36d..7743441b0 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,140 +1,51 @@ -diff --git a/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md b/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -index de7ad4681..37fe942aa 100644 ---- a/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -+++ b/docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -@@ -72,7 +72,7 @@ - - - [ ] Alfa - Controls - - [ ] Alfa - Input Mapping --- [ ] Alfa - Hitboxes -+- [ ] Delta - Hitboxes - - ### Rules - -@@ -252,5 +252,6 @@ Current OWNER clarification: - - [x] Delta - Event system audit - - Completed by PR_26175_DELTA_005_Runtime_Technical_Debt_Cleanup. - - [ ] Delta - Controls runtime framework audit -+- [ ] Delta - Hitboxes - - [ ] Delta - Object runtime framework audit - - [ ] Delta - World runtime framework audit -diff --git a/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md b/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -index bc548346d..1e2e47843 100644 ---- a/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -+++ b/docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -@@ -49,6 +49,7 @@ Team Charlie System Health owns: - - Shared JS - - API clients - - Event systems -+- Hitboxes - - Performance - - Technical debt remediation - - Runtime test coverage -diff --git a/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership.md b/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership.md -new file mode 100644 -index 000000000..aecd58bab ---- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership.md -@@ -0,0 +1,33 @@ -+# PR_26177_DELTA_001-hitboxes-team-ownership -+ -+Team: Delta -+Branch: PR_26177_DELTA_001-hitboxes-team-ownership -+Base: main -+Scope: Project Instructions ownership/backlog documentation only -+ -+## Summary -+ -+Team Delta is now the sole documented owner of Hitboxes in the Project Instructions ownership and backlog routing files. -+ -+## Changes -+ -+- Updated `docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md` to add Hitboxes to Team Delta ownership. -+- Updated `docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md` to change the Game Journey MVP Hitboxes item from Alfa to Delta. -+- Added Hitboxes to the Team Delta backlog alignment list. -+ -+## Scope Guard -+ -+- No implementation code changed. -+- No engine core files changed. -+- No `start_of_day` files changed. -+- No other team ownership assignments changed. -+- Existing unrelated untracked file `docs_build/dev/ProjectInstructions (2).zip` was left untouched. -+ -+## Validation -+ -+- PASS: Current branch before PR branch creation was `main`. -+- PASS: PR branch created and work remained on `PR_26177_DELTA_001-hitboxes-team-ownership`. -+- PASS: `rg -n "Alfa - Hitboxes" docs_build/dev/ProjectInstructions` returned no matches. -+- PASS: `rg -n "Delta - Hitboxes|Hitboxes" docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md` found only Delta ownership entries for Hitboxes in target files. -+- PASS: `git diff --name-only` showed only Project Instructions ownership/backlog files before report generation. -+ -diff --git a/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_branch-validation.md b/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_branch-validation.md -new file mode 100644 -index 000000000..4dcb5c321 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_branch-validation.md -@@ -0,0 +1,17 @@ -+# PR_26177_DELTA_001 Hitboxes Team Ownership Branch Validation -+ -+Branch: PR_26177_DELTA_001-hitboxes-team-ownership -+Base: main -+ -+Result: PASS -+ -+| Check | Status | Notes | -+|---|---|---| -+| Start branch was main | PASS | `git branch --show-current` returned `main` before branch creation. | -+| PR branch created | PASS | Current branch is `PR_26177_DELTA_001-hitboxes-team-ownership`. | -+| Branch starts from current main | PASS | Starting commit was `f237619cf7aea710a32b9d5141115fb02dbd3293`; `HEAD...origin/main` was `0 0` before edits. | -+| Scope is documentation/reporting only | PASS | No implementation code changed. | -+| Engine core untouched | PASS | No engine core files changed. | -+| start_of_day untouched | PASS | No `start_of_day` files changed. | -+| Unrelated work preserved | PASS | Existing untracked `docs_build/dev/ProjectInstructions (2).zip` left untouched. | -+ -diff --git a/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_validation-checklist.md b/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_validation-checklist.md -new file mode 100644 -index 000000000..9d801e2e9 ---- /dev/null -+++ b/docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_validation-checklist.md -@@ -0,0 +1,15 @@ -+# PR_26177_DELTA_001 Hitboxes Team Ownership Validation Checklist -+ -+| Requirement | Status | Notes | -+|---|---|---| -+| Hard stop unless current branch is main | PASS | Start branch was verified as `main` before creating the PR branch. | -+| Create and switch to PR branch | PASS | Created and switched to `PR_26177_DELTA_001-hitboxes-team-ownership`. | -+| Team Delta sole owner of Hitboxes | PASS | Team ownership map lists Hitboxes under Team Delta. | -+| Remove Hitboxes from Team Alfa ownership | PASS | Backlog item changed from `Alfa - Hitboxes` to `Delta - Hitboxes`; no `Alfa - Hitboxes` matches remain in ProjectInstructions. | -+| Do not modify implementation code | PASS | Only Project Instructions and report artifacts changed. | -+| Do not change other team ownership | PASS | Diff changes only Hitboxes ownership/routing. | -+| Regenerate Codex review diff | PASS | `docs_build/dev/reports/codex_review.diff` generated from the working diff. | -+| Regenerate Codex changed files | PASS | `docs_build/dev/reports/codex_changed_files.txt` generated from changed files. | -+| PR-specific report | PASS | `docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership.md` added. | -+| Branch validation | PASS | `docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_branch-validation.md` added. | -+ -diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index 9bbcdda40..0172ab7b9 100644 ---- a/docs_build/dev/reports/codex_changed_files.txt -+++ b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,12 +1,7 @@ --# git status --short --M docs_build/dev/reports/PR_26175_DELTA_010-final-team-delta-completion-report.md -- M docs_build/dev/reports/PR_26175_DELTA_010-runtime-testability-closeout.md --?? docs_build/dev/reports/PR_26175_DELTA_EOD_final_report.md -- --# git ls-files --others --exclude-standard --docs_build/dev/reports/PR_26175_DELTA_EOD_final_report.md -- --# git diff --stat --.../PR_26175_DELTA_010-final-team-delta-completion-report.md | 6 +++++- -- .../dev/reports/PR_26175_DELTA_010-runtime-testability-closeout.md | 3 ++- -- 2 files changed, 7 insertions(+), 2 deletions(-) -\ No newline at end of file -+docs_build/dev/ProjectInstructions/backlog/BACKLOG_MASTER.md -+docs_build/dev/ProjectInstructions/team_assignments/team_ownership.md -+docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership.md -+docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_branch-validation.md -+docs_build/dev/reports/PR_26177_DELTA_001-hitboxes-team-ownership_validation-checklist.md -+docs_build/dev/reports/codex_changed_files.txt -+docs_build/dev/reports/codex_review.diff +diff --git a/docs_build/dev/reports/coverage_changed_js_guardrail.txt b/docs_build/dev/reports/coverage_changed_js_guardrail.txt +index e9067da4f..7b1c51f19 100644 +--- a/docs_build/dev/reports/coverage_changed_js_guardrail.txt ++++ b/docs_build/dev/reports/coverage_changed_js_guardrail.txt +@@ -6,11 +6,7 @@ Missing changed runtime JS files are WARN, not FAIL. + Source: Playwright/Chromium built-in V8 coverage from the active Playwright run. + + Changed runtime JS files considered: +-(0%) src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +-(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +-(36%) src/shared/toolbox/tool-metadata-inventory.js - executed lines 2043/2043; executed functions 12/33 ++(100%) none changed - no changed runtime JS files + + Guardrail warnings: +-(0%) src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js - WARNING: changed runtime JS file missing from coverage; advisory only +-(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file missing from coverage; advisory only +-(36%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: advisory low coverage below 50%; executed lines 2043/2043; executed functions 12/33 ++(100%) none changed - no changed runtime JS files +diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt +index a298305f3..2f012df4d 100644 +--- a/docs_build/dev/reports/playwright_v8_coverage_report.txt ++++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt +@@ -17,9 +17,7 @@ Exercised tool entry points detected: + (81%) Theme V2 Shared JS - exercised 5 runtime JS files + + Changed runtime JS files covered: +-(0%) src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +-(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: changed runtime JS file was not collected by Playwright V8 coverage; advisory only +-(36%) src/shared/toolbox/tool-metadata-inventory.js - executed lines 2043/2043; executed functions 12/33 ++(100%) none changed - no changed runtime JS files + + Files with executed line/function counts where available: + (25%) assets/toolbox/assets/js/index.js - executed lines 1602/1602; executed functions 29/116 +@@ -47,15 +45,7 @@ Files with executed line/function counts where available: + (100%) assets/theme-v2/js/toolbox-status-bar.js - executed lines 427/427; executed functions 37/37 + + Uncovered or low-coverage changed JS files: +-(0%) src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js - WARNING: uncovered changed runtime JS file; advisory only +-(0%) src/dev-runtime/server/local-api-router.mjs - WARNING: uncovered changed runtime JS file; advisory only +-(36%) src/shared/toolbox/tool-metadata-inventory.js - WARNING: advisory low coverage; executed lines 2043/2043 ++(100%) none changed - no changed runtime JS files + + Changed JS files considered: +-(0%) assets/toolbox/sprites/js/index.js - changed JS file not collected as browser runtime coverage +-(0%) src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js - changed JS file not collected as browser runtime coverage +-(0%) src/dev-runtime/server/local-api-router.mjs - changed JS file not collected as browser runtime coverage +-(0%) tests/dev-runtime/SpritesAssetRepository.test.mjs - changed JS file not collected as browser runtime coverage +-(0%) tests/playwright/tools/SpritesToolMvp.spec.mjs - changed JS file not collected as browser runtime coverage +-(0%) tests/playwright/tools/ToolboxRoutePages.spec.mjs - changed JS file not collected as browser runtime coverage +-(36%) src/shared/toolbox/tool-metadata-inventory.js - changed JS file with browser V8 coverage ++(100%) none - no changed JS files diff --git a/docs_build/dev/reports/playwright_v8_coverage_report.txt b/docs_build/dev/reports/playwright_v8_coverage_report.txt index f2363d3bf..2f012df4d 100644 --- a/docs_build/dev/reports/playwright_v8_coverage_report.txt +++ b/docs_build/dev/reports/playwright_v8_coverage_report.txt @@ -12,31 +12,40 @@ Note: entry percentages use function coverage when available, otherwise line cov Note: coverage entries are aggregated across every page/tool where coverageReporter.start(page) and coverageReporter.stop(page) ran. Exercised tool entry points detected: -(76%) Toolbox Index - exercised 1 runtime JS files +(92%) Toolbox Index - exercised 3 runtime JS files (0%) Tool Template V2 - not exercised by this Playwright run -(72%) Theme V2 Shared JS - exercised 4 runtime JS files +(81%) Theme V2 Shared JS - exercised 5 runtime JS files Changed runtime JS files covered: (100%) none changed - no changed runtime JS files Files with executed line/function counts where available: -(36%) src/shared/toolbox/tool-metadata-inventory.js - executed lines 2041/2041; executed functions 12/33 -(53%) src/api/server-api-client.js - executed lines 168/168; executed functions 10/19 -(64%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1046/1046; executed functions 63/98 +(25%) assets/toolbox/assets/js/index.js - executed lines 1602/1602; executed functions 29/116 +(25%) src/api/session-api-client.js - executed lines 67/67; executed functions 3/12 +(36%) src/shared/toolbox/tool-metadata-inventory.js - executed lines 2043/2043; executed functions 12/33 +(42%) assets/toolbox/tags/js/index.js - executed lines 251/251; executed functions 8/19 +(50%) toolbox/game-hub/game-hub-api-client.js - executed lines 26/26; executed functions 2/4 +(57%) assets/toolbox/game-journey/js/index.js - executed lines 1662/1662; executed functions 82/144 +(58%) src/api/server-api-client.js - executed lines 168/168; executed functions 11/19 +(59%) assets/toolbox/colors/js/index.js - executed lines 1859/1859; executed functions 122/207 (65%) src/api/public-config-client.js - executed lines 209/209; executed functions 17/26 +(67%) assets/js/shared/assets-api-client.js - executed lines 19/19; executed functions 2/3 (67%) src/api/game-journey-completion-api-client.js - executed lines 15/15; executed functions 2/3 -(73%) assets/toolbox/game-journey/js/index.js - executed lines 1662/1662; executed functions 108/148 -(76%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 22/29 +(68%) assets/toolbox/idea-board/js/index.js - executed lines 764/764; executed functions 40/59 +(74%) assets/theme-v2/js/gamefoundry-partials.js - executed lines 1046/1046; executed functions 75/102 (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 -(89%) assets/theme-v2/js/toolbox-status-bar.js - executed lines 427/427; executed functions 32/36 +(80%) src/api/admin-owner-navigation.js - executed lines 42/42; executed functions 4/5 +(83%) src/api/toolbox-votes-api-client.js - executed lines 46/46; executed functions 5/6 +(90%) toolbox/tool-registry-api-client.js - executed lines 155/155; executed functions 26/29 +(91%) assets/theme-v2/js/admin-owner-navigation.js - executed lines 58/58; executed functions 10/11 +(93%) admin/tool-votes.js - executed lines 406/406; executed functions 53/57 +(94%) toolbox/tools-page-accordions.js - executed lines 1156/1156; executed functions 112/119 (100%) assets/js/shared/game-journey-api-client.js - executed lines 19/19; executed functions 2/2 +(100%) assets/theme-v2/js/toolbox-status-bar.js - executed lines 427/427; executed functions 37/37 Uncovered or low-coverage changed JS files: (100%) none changed - no changed runtime JS files Changed JS files considered: -(0%) scripts/validate-browser-env-agnostic.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/dev-runtime/GameJourneyCompletionMetricsStore.test.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/playwright/tools/AdminHealthOperationsPage.spec.mjs - changed JS file not collected as browser runtime coverage -(0%) tests/playwright/tools/GameJourneyTool.spec.mjs - changed JS file not collected as browser runtime coverage +(100%) none - no changed JS files diff --git a/src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js b/src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js index 407a7339e..48aa55a91 100644 --- a/src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js +++ b/src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js @@ -2530,6 +2530,333 @@ export function createAssetToolMockRepository(options = {}) { return paletteRepository.seedActiveProjectPalette(); } + const SPRITE_STATUS_OPTIONS = Object.freeze(["Draft", "Ready", "Archived"]); + + function normalizeSpriteStatus(value, fallback = "Ready") { + const normalized = normalizeText(value); + const match = SPRITE_STATUS_OPTIONS.find((status) => status.toLowerCase() === normalized.toLowerCase()); + return match || fallback; + } + + function normalizeSpriteCategory(value, fallback = "Sprite") { + const normalized = normalizeCatalogUsage(value); + return normalized || fallback; + } + + function paletteColorExists(paletteColorKey) { + if (!paletteColorKey) { + return true; + } + return listPaletteSwatches().some((swatch) => swatch.key === paletteColorKey); + } + + function spritePaletteColors() { + return listPaletteSwatches().map((swatch) => ({ + hex: swatch.hex || "", + key: swatch.key, + name: swatch.name, + source: swatch.source || "Palette/Colors", + })); + } + + function spriteReferenceSummary(asset = null) { + return { + destructiveDeleteAllowed: false, + message: asset + ? "Object and World reference contracts are not connected yet; destructive delete is disabled for Sprites. Archive remains available." + : "Reference contract is ready for future Object and World usage checks.", + references: [], + status: "Unavailable", + usageCount: 0, + }; + } + + function decorateSpriteAsset(asset) { + if (!asset) { + return null; + } + const paletteColorKey = normalizeText(asset.paletteColorKey); + const referenceSummary = spriteReferenceSummary(asset); + return { + ...asset, + category: normalizeSpriteCategory(asset.category || "Sprite"), + dimensions: asset.dimensions || null, + paletteColorKey, + paletteColorName: spritePaletteColors().find((swatch) => swatch.key === paletteColorKey)?.name || "", + previewStatus: asset.viewPath || asset.storedPath + ? "Preview metadata available; image bytes are unavailable until storage preview is connected." + : "Preview unavailable until sprite image storage is connected.", + referenceSummary, + status: normalizeSpriteStatus(asset.status), + usageCount: referenceSummary.usageCount, + }; + } + + function listSpriteRecords() { + return listAssetsByType("Sprites").map(decorateSpriteAsset); + } + + function spriteValidationResult(input = {}, existingAsset = null) { + const name = normalizeText(input.name || existingAsset?.name); + const description = normalizeText(input.description || existingAsset?.description); + const category = normalizeSpriteCategory(input.category || existingAsset?.category || "Sprite"); + const status = normalizeSpriteStatus(input.status || existingAsset?.status); + const tags = listTags(); + const tagKeys = normalizeAssetTagKeys(input.tagKeys, tags); + const requestedTagKeys = Array.isArray(input.tagKeys) ? input.tagKeys.map(normalizeText).filter(Boolean) : []; + const invalidTagKeys = requestedTagKeys.filter((tagKey) => !tagKeys.includes(tagKey)); + const paletteColorKey = normalizeText(input.paletteColorKey || existingAsset?.paletteColorKey); + const findings = []; + + if (!name) { + findings.push({ + action: "Name the sprite before saving.", + field: "name", + label: "Sprite Name", + }); + } + if (input.status && !SPRITE_STATUS_OPTIONS.includes(status)) { + findings.push({ + action: `Choose a valid sprite status: ${SPRITE_STATUS_OPTIONS.join(", ")}.`, + field: "status", + label: "Sprite Status", + }); + } + if (input.category && !normalizeCatalogUsage(input.category)) { + findings.push({ + action: `Choose a valid sprite category: ${ASSET_USAGE_OPTIONS.join(", ")}.`, + field: "category", + label: "Sprite Category", + }); + } + if (invalidTagKeys.length) { + findings.push({ + action: "Choose tags from the shared Tags tool list.", + field: "tagKeys", + label: "Sprite Tags", + }); + } + if (!paletteColorExists(paletteColorKey)) { + findings.push({ + action: "Choose a Palette/Colors record by key; Sprites cannot own reusable color definitions.", + field: "paletteColorKey", + label: "Palette Color Key", + }); + } + + return { + findings, + sprite: { + category, + description, + name, + paletteColorKey, + status, + tagKeys, + }, + status: findings.length === 0 ? "Ready" : "Needs Input", + }; + } + + function duplicateSpriteFinding(name, currentAssetId = "") { + const normalizedName = normalizeText(name).toLowerCase(); + const spriteKey = spriteAssetKeyForObjectKey(name); + const duplicate = listSpriteRecords().find((asset) => ( + asset.id !== currentAssetId + && ( + asset.id === spriteKey + || normalizeText(asset.name).toLowerCase() === normalizedName + ) + )); + if (!duplicate) { + return null; + } + return { + action: `A sprite named ${duplicate.name} already exists. Edit that sprite or choose a unique name.`, + field: "name", + label: "Duplicate Sprite", + }; + } + + function finishSpriteMutation(asset, validation, message, extra = {}) { + const timestamp = new Date().toISOString(); + asset.name = validation.sprite.name; + asset.description = validation.sprite.description; + asset.category = validation.sprite.category; + asset.paletteColorKey = validation.sprite.paletteColorKey; + asset.status = validation.sprite.status; + asset.tagKeys = validation.sprite.tagKeys; + asset.updatedAt = timestamp; + asset.updatedBy = activeUserKey(); + selectedAssetId = asset.id; + replaceValidationRows(asset.projectId, []); + persistTables(); + return { + asset: decorateSpriteAsset(asset), + message, + snapshot: getSpritesSnapshot(), + validation, + ...extra, + }; + } + + function createSpriteRecord(input = {}) { + const validation = spriteValidationResult(input); + const duplicateFinding = validation.findings.length ? null : duplicateSpriteFinding(validation.sprite.name); + const findings = duplicateFinding ? [...validation.findings, duplicateFinding] : validation.findings; + if (hasExplicitGuestSession()) { + return { + created: false, + message: "Sign in required to save Sprites through the Local API.", + snapshot: getSpritesSnapshot(), + validation: { ...validation, findings, status: "Needs Input" }, + }; + } + const projectResult = ensureUploadProject(); + const projectId = projectResult.projectId || getConfigurationHandoff().activeProject?.id || ""; + replaceValidationRows(projectId, findings); + if (findings.length || !projectId) { + return { + created: false, + message: findings[0]?.action || projectResult.message || "Sprite create blocked: open an active game first.", + snapshot: getSpritesSnapshot(), + validation: { ...validation, findings, status: "Needs Input" }, + }; + } + + const handoffResult = ensureSpriteAssetForObject({ + name: validation.sprite.name, + objectKey: validation.sprite.name, + objectName: validation.sprite.name, + }); + if (!handoffResult.created && !handoffResult.linked) { + return { + created: false, + message: handoffResult.message, + snapshot: getSpritesSnapshot(), + validation, + }; + } + return finishSpriteMutation( + handoffResult.asset, + validation, + `Created sprite ${validation.sprite.name}.`, + { created: true }, + ); + } + + function updateSpriteRecord(assetId, input = {}) { + const handoff = getConfigurationHandoff(); + const projectId = handoff.activeProject?.id || ""; + const asset = findOwnedAsset(assetId, projectId); + if (!asset) { + return { + message: blockedOwnerMessage("update"), + snapshot: getSpritesSnapshot(), + updated: false, + }; + } + const validation = spriteValidationResult(input, asset); + const duplicateFinding = validation.findings.length ? null : duplicateSpriteFinding(validation.sprite.name, asset.id); + const findings = duplicateFinding ? [...validation.findings, duplicateFinding] : validation.findings; + replaceValidationRows(projectId, findings); + if (hasExplicitGuestSession()) { + return { + message: "Sign in required to save Sprites through the Local API.", + snapshot: getSpritesSnapshot(), + updated: false, + validation: { ...validation, findings, status: "Needs Input" }, + }; + } + if (findings.length) { + return { + asset: decorateSpriteAsset(asset), + message: findings[0]?.action || "Sprite update blocked.", + snapshot: getSpritesSnapshot(), + updated: false, + validation: { ...validation, findings, status: "Needs Input" }, + }; + } + return finishSpriteMutation( + asset, + validation, + `Updated sprite ${validation.sprite.name}.`, + { updated: true }, + ); + } + + function archiveSpriteRecord(assetId) { + const handoff = getConfigurationHandoff(); + const projectId = handoff.activeProject?.id || ""; + const asset = findOwnedAsset(assetId, projectId); + if (!asset) { + return { + archived: false, + message: blockedOwnerMessage("archive"), + snapshot: getSpritesSnapshot(), + }; + } + if (hasExplicitGuestSession()) { + return { + archived: false, + message: "Sign in required to archive Sprites through the Local API.", + snapshot: getSpritesSnapshot(), + }; + } + const timestamp = new Date().toISOString(); + asset.archivedAt = timestamp; + asset.status = "Archived"; + asset.updatedAt = timestamp; + asset.updatedBy = activeUserKey(); + selectedAssetId = asset.id; + replaceValidationRows(projectId, []); + persistTables(); + return { + archived: true, + asset: decorateSpriteAsset(asset), + message: `Archived sprite ${asset.name}.`, + snapshot: getSpritesSnapshot(), + }; + } + + function deleteSpriteRecord(assetId) { + const asset = findOwnedAsset(assetId, getConfigurationHandoff().activeProject?.id || ""); + return { + asset: decorateSpriteAsset(asset), + deleted: false, + message: "Destructive sprite delete is disabled until Object and World references can be verified. Archive the sprite instead.", + referenceSummary: spriteReferenceSummary(asset), + snapshot: getSpritesSnapshot(), + }; + } + + function getSpritesSnapshot() { + const handoff = getConfigurationHandoff(); + const sprites = listSpriteRecords(); + const selectedSprite = sprites.find((asset) => asset.id === selectedAssetId) || sprites[0] || null; + const projectId = handoff.activeProject?.id || ""; + const findings = tables.asset_validation_items.filter((row) => row.projectId === projectId); + return { + categories: [...ASSET_USAGE_OPTIONS], + handoff, + paletteColors: spritePaletteColors(), + paletteOwnership: "Palette/Colors is the reusable color source of truth. Sprites store paletteColorKey references only.", + preview: { + message: "Sprite image preview is product-safe metadata until storage/image bytes are connected.", + status: "Unavailable", + }, + referenceContract: spriteReferenceSummary(), + selectedSprite, + sprites, + statusOptions: [...SPRITE_STATUS_OPTIONS], + tags: listTags(), + validation: { + findings, + status: findings.length ? "Needs Input" : "Ready", + }, + }; + } + function getProgressHandoff() { const handoff = getConfigurationHandoff(); const assets = listAssets(); @@ -2610,15 +2937,19 @@ export function createAssetToolMockRepository(options = {}) { ASSET_USAGE_OPTIONS, ASSET_USAGE_BY_ROLE, addAssetRecord, + archiveSpriteRecord, assetsByType, clearAssetLibrary, deleteAsset, deleteAssetRecord, + deleteSpriteRecord, ensureSpriteAssetForObject, + createSpriteRecord, getConfigurationHandoff, getPaletteSnapshot, getProgressHandoff, getRoleDiagnostics, + getSpritesSnapshot, getSnapshot, getTables, importAsset, @@ -2642,6 +2973,7 @@ export function createAssetToolMockRepository(options = {}) { setUploadFileWriteSupport, updateAsset, updateAssetRecord, + updateSpriteRecord, validateCatalogAssetInput, validateAssetInput }; diff --git a/src/dev-runtime/server/local-api-router.mjs b/src/dev-runtime/server/local-api-router.mjs index 913163236..897d6630d 100644 --- a/src/dev-runtime/server/local-api-router.mjs +++ b/src/dev-runtime/server/local-api-router.mjs @@ -2555,7 +2555,7 @@ function normalizedToolKey(row) { return String(row?.toolKey || row?.toolId || row?.id || "").trim(); } -const SOURCE_CONTROLLED_TOOLBOX_TOOL_IDS = new Set(["game-hub", "idea-board", "messages", "tags", "text-to-speech", "users"]); +const SOURCE_CONTROLLED_TOOLBOX_TOOL_IDS = new Set(["game-hub", "idea-board", "messages", "sprites", "tags", "text-to-speech", "users"]); const SOURCE_CONTROLLED_TOOLBOX_METADATA_FIELDS = Object.freeze([ "active", "adminOnly", @@ -6640,6 +6640,7 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, if (toolId === "tags") return this.tagsRepository; if (toolId === "asset") return this.assetRepository; if (toolId === "assets") return this.assetRepository; + if (toolId === "sprites") return this.assetRepository; throw new Error(`Unknown toolbox API data source: ${toolId}.`); } @@ -6706,7 +6707,7 @@ SELECT pg_database_size(current_database()) AS database_size_bytes, TAGS_TOOL_TABLES: this.tagsRepository.TAGS_TOOL_TABLES, }; } - if (toolId === "asset" || toolId === "assets") { + if (toolId === "asset" || toolId === "assets" || toolId === "sprites") { return { ASSET_ROLE_DEFINITIONS: this.assetRepository.ASSET_ROLE_DEFINITIONS, ASSET_CATALOG_TYPES: this.assetRepository.ASSET_CATALOG_TYPES, diff --git a/src/shared/toolbox/tool-metadata-inventory.js b/src/shared/toolbox/tool-metadata-inventory.js index 955f54d8a..9c4bd342c 100644 --- a/src/shared/toolbox/tool-metadata-inventory.js +++ b/src/shared/toolbox/tool-metadata-inventory.js @@ -472,9 +472,11 @@ export const TOOL_REGISTRY = Object.freeze([ "requiredForPublish": true, "requires": [], "status": "Wireframe", + "releaseChannel": "wireframe", "progressChecklist": [ - "Review readiness", - "Static planned text only" + "Toolbox entry opens Sprites route", + "API-backed sprite library shell available", + "Palette/Colors remains reusable color source of truth" ], "deferred": false, "hidden": false, diff --git a/tests/dev-runtime/SpritesAssetRepository.test.mjs b/tests/dev-runtime/SpritesAssetRepository.test.mjs new file mode 100644 index 000000000..6d4e4019d --- /dev/null +++ b/tests/dev-runtime/SpritesAssetRepository.test.mjs @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; + +import { createAssetToolMockRepository } from "../../src/dev-runtime/persistence/tool-repositories/assets-mock-repository.js"; +import { MOCK_DB_KEYS } from "../../src/dev-runtime/persistence/mock-db-store.js"; + +export function run() { + const repository = createAssetToolMockRepository({ + sessionUserKey: () => MOCK_DB_KEYS.users.user1, + }); + + const emptySnapshot = repository.getSpritesSnapshot(); + assert.equal(emptySnapshot.validation.status, "Ready"); + assert.equal(emptySnapshot.paletteOwnership.includes("Palette/Colors"), true); + assert.deepEqual(emptySnapshot.sprites, []); + + const created = repository.createSpriteRecord({ + category: "Character", + name: "Hero Sprite", + paletteColorKey: "", + status: "Ready", + tagKeys: [], + }); + assert.equal(created.created, true); + assert.equal(created.asset.name, "Hero Sprite"); + assert.equal(created.asset.category, "Character"); + assert.equal(created.asset.createdBy, MOCK_DB_KEYS.users.user1); + assert.equal(created.asset.updatedBy, MOCK_DB_KEYS.users.user1); + assert.equal(created.asset.paletteColorKey, ""); + assert.equal(created.asset.usage, "sprite"); + assert.equal(created.asset.referenceSummary.destructiveDeleteAllowed, false); + assert.equal(repository.getSpritesSnapshot().sprites.length, 1); + + const duplicate = repository.createSpriteRecord({ + category: "Character", + name: "Hero Sprite", + status: "Ready", + tagKeys: [], + }); + assert.equal(duplicate.created, false); + assert.match(duplicate.message, /already exists/); + + const updated = repository.updateSpriteRecord(created.asset.id, { + category: "Icon", + name: "Hero Sprite Revised", + paletteColorKey: "", + status: "Ready", + tagKeys: [], + }); + assert.equal(updated.updated, true); + assert.equal(updated.asset.name, "Hero Sprite Revised"); + assert.equal(updated.asset.category, "Icon"); + + const blockedDelete = repository.deleteSpriteRecord(updated.asset.id); + assert.equal(blockedDelete.deleted, false); + assert.equal(blockedDelete.referenceSummary.destructiveDeleteAllowed, false); + assert.match(blockedDelete.message, /Destructive sprite delete is disabled/); + + const archived = repository.archiveSpriteRecord(updated.asset.id); + assert.equal(archived.archived, true); + assert.equal(archived.asset.status, "Archived"); + + const guestRepository = createAssetToolMockRepository({ sessionUserKey: () => "" }); + const guestCreate = guestRepository.createSpriteRecord({ + category: "Sprite", + name: "Guest Sprite", + status: "Ready", + tagKeys: [], + }); + assert.equal(guestCreate.created, false); + assert.match(guestCreate.message, /Sign in required/); +} diff --git a/tests/playwright/tools/SpritesToolMvp.spec.mjs b/tests/playwright/tools/SpritesToolMvp.spec.mjs new file mode 100644 index 000000000..95f8f0374 --- /dev/null +++ b/tests/playwright/tools/SpritesToolMvp.spec.mjs @@ -0,0 +1,147 @@ +import { expect, test } from "@playwright/test"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { MOCK_DB_KEYS } from "../../../src/dev-runtime/persistence/mock-db-store.js"; +import { isBrowserExtensionNoise } from "../../helpers/browserExtensionNoise.mjs"; +import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; +import { workspaceV2CoverageReporter } from "../../helpers/workspaceV2CoverageReporter.mjs"; + +const REPO_ROOT = resolve(fileURLToPath(new URL("../../..", import.meta.url))); + +test.afterAll(async () => { + await workspaceV2CoverageReporter.writeReport(); +}); + +async function setServerSession(server, userKey = MOCK_DB_KEYS.users.user1) { + await fetch(`${server.baseUrl}/api/session/mode`, { + body: JSON.stringify({ modeId: "local-db" }), + headers: { "Content-Type": "application/json" }, + method: "POST", + }); + await fetch(`${server.baseUrl}/api/session/user`, { + body: JSON.stringify({ userKey }), + headers: { "Content-Type": "application/json" }, + method: "POST", + }); +} + +async function openSpritesPage(page, userKey = MOCK_DB_KEYS.users.user1) { + const server = await startRepoServer(); + const failedRequests = []; + const pageErrors = []; + const consoleErrors = []; + await page.addInitScript(({ apiUrl, siteUrl }) => { + window.GameFoundryPublicConfig = { + apiUrl, + environmentLabel: "Development Environment", + siteUrl, + }; + }, { apiUrl: `${server.baseUrl}/api`, siteUrl: server.baseUrl }); + page.on("response", (response) => { + if (response.status() >= 400) { + failedRequests.push(`${response.status()} ${response.url()}`); + } + }); + page.on("requestfailed", (request) => { + failedRequests.push(`FAILED ${request.url()}`); + }); + page.on("pageerror", (error) => { + const text = error.stack || error.message; + if (!isBrowserExtensionNoise(text)) { + pageErrors.push(error.message); + } + }); + page.on("console", (message) => { + if (message.type() === "error" && !isBrowserExtensionNoise(message.text())) { + consoleErrors.push(message.text()); + } + }); + await setServerSession(server, userKey); + await workspaceV2CoverageReporter.start(page); + await page.goto(`${server.baseUrl}/toolbox/sprites/index.html`, { waitUntil: "networkidle" }); + return { consoleErrors, failedRequests, pageErrors, server }; +} + +function expectNoPageFailures(failures) { + expect(failures.failedRequests).toEqual([]); + expect(failures.pageErrors).toEqual([]); + expect(failures.consoleErrors).toEqual([]); +} + +test("Sprites is clickable from Toolbox and loads the API-backed MVP surface", async ({ page }) => { + const failures = await openSpritesPage(page); + try { + await page.goto(`${failures.server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); + const spritesLink = page.locator("[data-toolbox-tool-name-link='Sprites']"); + await expect(spritesLink).toHaveAttribute("href", "/toolbox/sprites/index.html"); + await expect(spritesLink).not.toHaveAttribute("data-toolbox-launch-blocked", "planned"); + await spritesLink.click(); + await page.waitForURL(/\/toolbox\/sprites\/index\.html$/); + await expect(page.getByRole("heading", { level: 1, name: "Sprites" })).toBeVisible(); + await expect(page.locator("[data-sprites-table]")).toBeVisible(); + await expect(page.locator("[data-sprites-log]")).toContainText(/Sprites ready|Sprites loading/); + await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); + expectNoPageFailures(failures); + } finally { + await workspaceV2CoverageReporter.stop(page); + await failures.server.close(); + } +}); + +test("Sprites creator can create, edit, filter, inspect, and archive a sprite", async ({ page }) => { + const failures = await openSpritesPage(page); + try { + await page.getByRole("button", { name: "Add Sprite" }).click(); + await page.getByLabel("Sprite name").fill("Hero Sprite"); + await page.getByLabel("Sprite category").selectOption("Character"); + await page.getByRole("button", { name: "Save" }).click(); + + await expect(page.locator("[data-sprites-row]").filter({ hasText: "Hero Sprite" })).toBeVisible(); + await expect(page.locator("[data-sprites-log]")).toContainText("Created sprite Hero Sprite."); + await expect(page.locator("[data-sprites-metadata]")).toContainText("Key:"); + await expect(page.locator("[data-sprites-metadata]")).toContainText("Palette color key: None"); + await expect(page.locator("[data-sprites-preview]")).toContainText(/Preview metadata|Preview unavailable/); + await expect(page.locator("[data-sprites-references]")).toContainText("destructive delete is disabled"); + + await page.locator("[data-sprites-row]").filter({ hasText: "Hero Sprite" }).getByRole("button", { name: "Edit" }).click(); + await page.getByLabel("Sprite name").fill("Hero Sprite Revised"); + await page.getByLabel("Sprite category").selectOption("Icon"); + await page.getByRole("button", { name: "Save" }).click(); + await expect(page.locator("[data-sprites-row]").filter({ hasText: "Hero Sprite Revised" })).toBeVisible(); + await expect(page.locator("[data-sprites-row]").filter({ hasText: "Icon" })).toBeVisible(); + + await page.getByLabel("Search Sprites").fill("revised"); + await expect(page.locator("[data-sprites-row]")).toHaveCount(1); + await page.getByLabel("Search Sprites").fill(""); + await page.getByLabel("Filter Sprites by category").selectOption("Icon"); + await expect(page.locator("[data-sprites-row]")).toHaveCount(1); + + await page.locator("[data-sprites-row]").filter({ hasText: "Hero Sprite Revised" }).getByRole("button", { name: "Archive" }).click(); + await expect(page.locator("[data-sprites-row]").filter({ hasText: "Hero Sprite Revised" })).toContainText("Archived"); + await expect(page.locator("[data-sprites-log]")).toContainText("Archived sprite Hero Sprite Revised."); + expectNoPageFailures(failures); + } finally { + await workspaceV2CoverageReporter.stop(page); + await failures.server.close(); + } +}); + +test("Sprites guest save redirects to sign in and Palette color ownership stays external", async ({ page }) => { + const failures = await openSpritesPage(page, ""); + try { + await page.getByRole("button", { name: "Add Sprite" }).click(); + await page.getByLabel("Sprite name").fill("Guest Sprite"); + await page.getByRole("button", { name: "Save" }).click(); + await page.waitForURL(/\/account\/sign-in\.html$/); + + const source = readFileSync(resolve(REPO_ROOT, "assets/toolbox/sprites/js/index.js"), "utf8"); + expect(source).not.toMatch(/#[0-9A-Fa-f]{6}/); + expect(source).not.toMatch(/const\s+\w*color\w*\s*=\s*\[/i); + expect(source).toContain("paletteColorKey"); + expectNoPageFailures(failures); + } finally { + await workspaceV2CoverageReporter.stop(page); + await failures.server.close(); + } +}); diff --git a/tests/playwright/tools/ToolboxRoutePages.spec.mjs b/tests/playwright/tools/ToolboxRoutePages.spec.mjs index 243f413d0..181278600 100644 --- a/tests/playwright/tools/ToolboxRoutePages.spec.mjs +++ b/tests/playwright/tools/ToolboxRoutePages.spec.mjs @@ -564,10 +564,10 @@ test("toolbox index shows wireframe and beta tools while Planned remains opt-in" await expect(page.locator("[data-toolbox-tool-name-link='Game Hub']")).toHaveAttribute("href", "/toolbox/game-hub/index.html"); await expect(page.locator("[data-toolbox-tool-name-link='Text To Speech']")).toHaveAttribute("href", "/toolbox/text-to-speech/index.html"); await expect(page.locator("[data-toolbox-tool-name-link='Publish']")).toHaveCount(0); - await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 15/43"); + await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 16/43"); await page.locator("[data-toolbox-status-filter='planned']").click(); await expect(page.locator("[data-toolbox-status-filter='planned']")).toHaveAttribute("aria-pressed", "true"); - await expect(page.locator("[data-toolbox-tool-card][data-toolbox-release-channel='planned']")).toHaveCount(27); + await expect(page.locator("[data-toolbox-tool-card][data-toolbox-release-channel='planned']")).toHaveCount(26); await expect(page.locator("[data-toolbox-tool-card]")).toHaveCount(42); await expect(page.locator("[data-tools-count]")).toHaveText("Tool Count: 42/43"); await expect(page.locator("[data-toolbox-tool-name-link='AI Command Center']")).toBeVisible(); @@ -641,8 +641,8 @@ test("toolbox status kickers, filters, card order, and voting controls work from await page.goto(`${server.baseUrl}/toolbox/index.html`, { waitUntil: "networkidle" }); await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (27)", - "Wireframe (4)", + "Planned (26)", + "Wireframe (5)", "Beta (8)", "Complete (3)", "Deprecated (1)", @@ -658,8 +658,8 @@ test("toolbox status kickers, filters, card order, and voting controls work from await page.locator("[data-tools-view='build-path']").click(); await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (27)", - "Wireframe (4)", + "Planned (26)", + "Wireframe (5)", "Beta (8)", "Complete (3)", "Deprecated (1)", @@ -1378,8 +1378,8 @@ test("toolbox Build Path status filters support multi-select registry-matched to await expect(page.locator("[data-tools-sort='grouped']")).not.toHaveClass(/primary/); await expect(page.locator("[data-toolbox-status-filter]")).toHaveText([ - "Planned (27)", - "Wireframe (4)", + "Planned (26)", + "Wireframe (5)", "Beta (8)", "Complete (3)", "Deprecated (1)", @@ -1391,14 +1391,14 @@ test("toolbox Build Path status filters support multi-select registry-matched to await page.locator("[data-toolbox-status-filter='planned']").click(); await expectActiveFilters(["planned", "complete"]); - await expectBuildPathChannels(["planned", "complete"], 30); + await expectBuildPathChannels(["planned", "complete"], 29); await expect(page.locator("[data-build-path-tool='AI Command Center']")).toBeVisible(); await expectBuildPathOrder("AI Command Center", registryById.get("ai-assistant").order); await expectBuildPathOrder("Colors", registryById.get("colors").order); await page.locator("[data-toolbox-status-filter='complete']").click(); await expectActiveFilters(["planned"]); - await expectBuildPathChannels(["planned"], 27); + await expectBuildPathChannels(["planned"], 26); await expect(page.locator("[data-build-path-tool='Colors']")).toHaveCount(0); await expect(page.locator("[data-build-path-tool='AI Command Center']")).toBeVisible(); diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html index c66454371..4c47fdd27 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html @@ -6,7 +6,7 @@ Sprites - GameFoundryStudio - + @@ -18,35 +18,101 @@
Toolbox / Sprites

Sprites

-

Plan sprite creation, review, and game-ready export workflows. Static wireframe only; no database, persistence, save, load, or runtime behavior is implemented.

+

Manage sprite asset records for creators, games, worlds, publishing, sharing, and community workflows.

-
+
-
-

Workspace

-

Plan sprite creation, review, and game-ready export workflows. This page preserves the shared Theme V2 tool template structure for future rebuild work.

+
+
+

Sprite Library

+

Use the table to create, edit, and archive sprite records through the Local API service contract.

+
+
LoadingLibrary Status
+
0Sprites
+
0Visible
+
+

Sprites loading.

+
+ + + + + + + + + + + + + + + +
NameStatusCategoryTagsPalette KeyUpdatedActions
Loading Sprites.
+
@@ -57,6 +123,7 @@

Inspector

+