Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions assets/toolbox/sprites/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
const SPRITES_API_PATH = "/api/sprites/records";

const elements = {
apiStatus: document.querySelector("[data-sprites-api-status]"),
count: document.querySelector("[data-sprites-count]"),
emptyState: document.querySelector("[data-sprites-empty-state]"),
errorState: document.querySelector("[data-sprites-error-state]"),
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]"),
paletteStatus: document.querySelector("[data-sprites-palette-status]"),
refresh: document.querySelector("[data-sprites-refresh]"),
tableBody: document.querySelector("[data-sprites-table-body]"),
updated: document.querySelector("[data-sprites-updated]"),
};

function setText(target, value) {
if (target) {
target.textContent = value;
}
}

function setHidden(target, hidden) {
if (target) {
target.hidden = hidden;
}
}

function createCell(value) {
const cell = document.createElement("td");
cell.textContent = value;
return cell;
}

function createHeaderCell(value) {
const cell = document.createElement("th");
cell.scope = "row";
cell.textContent = value;
return cell;
}

function normalizeText(value, fallback = "Unavailable") {
const text = String(value ?? "").trim();
return text || fallback;
}

function formatTimestamp(value) {
const text = String(value ?? "").trim();
if (!text) {
return "Unavailable";
}
const date = new Date(text);
if (Number.isNaN(date.getTime())) {
return text;
}
return date.toLocaleString();
}

function formatDimensions(sprite) {
const width = Number(sprite?.width ?? sprite?.dimensions?.width);
const height = Number(sprite?.height ?? sprite?.dimensions?.height);
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
return "Unavailable";
}
return `${width} x ${height}`;
}

function formatSource(sprite) {
return normalizeText(sprite?.sourceName || sprite?.sourcePath || sprite?.storagePath || sprite?.storageKey || sprite?.sourceStorageReference);
}

function paletteKeysFor(sprite) {
if (Array.isArray(sprite?.paletteColorKeys)) {
return sprite.paletteColorKeys.map((key) => String(key || "").trim()).filter(Boolean);
}
if (Array.isArray(sprite?.palette_color_keys)) {
return sprite.palette_color_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";
}

function spriteRowsFromPayload(payload) {
if (Array.isArray(payload?.data?.sprites)) {
return payload.data.sprites;
}
if (Array.isArray(payload?.sprites)) {
return payload.sprites;
}
return [];
}

function renderLoading() {
setText(elements.apiStatus, "Loading");
setText(elements.libraryStatus, "Loading");
setText(elements.outputStatus, "Loading");
setText(elements.outputSummary, "Waiting for Sprites API response.");
setText(elements.emptyState, "Loading Sprites records.");
setText(elements.updated, "Checking");
setHidden(elements.emptyState, false);
setHidden(elements.errorState, true);
if (elements.tableBody) {
const row = document.createElement("tr");
const cell = createCell("Loading Sprites records.");
cell.colSpan = 8;
row.append(cell);
elements.tableBody.replaceChildren(row);
}
}

function renderUnavailable(message) {
const detail = normalizeText(message, "Sprites API unavailable.");
setText(elements.apiStatus, "Unavailable");
setText(elements.libraryStatus, "Unavailable");
setText(elements.outputStatus, "Unavailable");
setText(elements.outputSummary, detail);
setText(elements.emptyState, "Sprites records cannot be loaded from the API yet.");
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.updated, new Date().toLocaleTimeString());
setHidden(elements.emptyState, false);
setHidden(elements.errorState, false);
if (elements.tableBody) {
const row = document.createElement("tr");
const cell = createCell("Sprites API unavailable.");
cell.colSpan = 8;
row.append(cell);
elements.tableBody.replaceChildren(row);
}
}

function renderPaletteStatus(sprites) {
const referencedKeys = new Set();
sprites.forEach((sprite) => {
paletteKeysFor(sprite).forEach((key) => referencedKeys.add(key));
});
if (referencedKeys.size === 0) {
setText(elements.paletteStatus, "No Palette/Colors references in current Sprites records.");
return;
}
setText(elements.paletteStatus, `${referencedKeys.size} Palette/Colors key reference${referencedKeys.size === 1 ? "" : "s"} surfaced from API records.`);
}

function renderRows(sprites) {
if (!elements.tableBody) {
return;
}
if (sprites.length === 0) {
const row = document.createElement("tr");
const cell = createCell("No Sprites records returned by the API.");
cell.colSpan = 8;
row.append(cell);
elements.tableBody.replaceChildren(row);
return;
}

const rows = sprites.map((sprite) => {
const row = document.createElement("tr");
const paletteKeys = paletteKeysFor(sprite);
row.dataset.spritesRowKey = normalizeText(sprite?.key, "");
row.append(
createHeaderCell(normalizeText(sprite?.name)),
createCell(normalizeText(sprite?.status)),
createCell(normalizeText(sprite?.category, "None")),
createCell(formatSource(sprite)),
createCell(formatDimensions(sprite)),
createCell(paletteKeys.length ? paletteKeys.join(", ") : "None"),
createCell(formatTimestamp(sprite?.updatedAt ?? sprite?.updated_at)),
createCell(usageCountFor(sprite))
);
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`);
});
return row;
});
elements.tableBody.replaceChildren(...rows);
}

function renderSprites(payload) {
const sprites = spriteRowsFromPayload(payload);
const count = sprites.length;
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.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.");
setHidden(elements.emptyState, count > 0);
setHidden(elements.errorState, true);
renderPaletteStatus(sprites);
renderRows(sprites);
}

async function loadSprites() {
renderLoading();
try {
const response = await fetch(SPRITES_API_PATH, {
cache: "no-store",
headers: { accept: "application/json" },
});
let payload = null;
try {
payload = await response.json();
} catch {
payload = null;
}
if (!response.ok || payload?.ok === false) {
const message = payload?.error?.message || payload?.message || `Sprites API returned ${response.status}.`;
renderUnavailable(message);
return;
}
renderSprites(payload || {});
} catch (error) {
renderUnavailable(error instanceof Error ? error.message : "Sprites API request failed.");
}
}

elements.refresh?.addEventListener("click", () => {
void loadSprites();
});

void loadSprites();
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# PR_26177_CHARLIE_011 Branch Validation

Status: PASS

## Checks

- PASS: Started from `main`.
- PASS: `main` was clean before the PR branch was created.
- PASS: `main` and `origin/main` were synced at `0 0` before the PR branch was created.
- PASS: Remote sync was refreshed with Git's Windows certificate backend after the default OpenSSL certificate store failed.
- PASS: Current work branch is `PR_26177_CHARLIE_011-sprites-tool-shell`.
- PASS: Branch contains only the Sprites shell PR scope.
- PASS: No merge was performed.
- PASS: No `start_of_day` path is changed.

## Notes

The branch intentionally contains uncommitted changes while this report is generated. Final clean state is verified after commit and push.
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# PR_26177_CHARLIE_011 Manual Validation Notes

Status: PASS

## Manual Review

- Reviewed the Sprites page markup for Theme V2 layout consistency with existing Toolbox tools.
- Verified page copy uses `Sprites`, not `Sprite Editor`.
- Verified the page presents Sprites as asset management, not image editing.
- Verified the tool does not create or duplicate Palette/Colors records.
- Verified the browser module renders only API response data and uses explicit unavailable states when the API route is missing.
- Verified the refresh control re-runs the API read action.
- Verified the table-first layout includes user-visible loading, empty, populated, and unavailable states through targeted Playwright tests.

## Manual Limitation

The API/database foundation is on `PR_26177_CHARLIE_010-sprites-api-db-foundation` and is not merged into `main` yet. This PR therefore validates the shell against mocked API responses and intentionally keeps a visible unavailable state for environments where the route is not present.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# PR_26177_CHARLIE_011 Requirements Checklist

Status: PASS

- PASS: Added Sprites tool route/shell under the current Toolbox structure.
- PASS: Tool title is `Sprites`.
- PASS: Used current GFS shell/layout patterns.
- PASS: Used Theme V2 classes and shared layout conventions.
- PASS: Navigation entry already exists in the shared Toolbox menu; no duplicate menu item was added.
- PASS: HTML uses external JavaScript only.
- PASS: Added loading, empty, and error/unavailable surfaces.
- PASS: Shell reads `/api/sprites/records` and does not own authoritative product data in the browser.
- PASS: Missing API is shown as an explicit unavailable state.
- PASS: Palette/Colors is documented in-page as the reusable color source of truth.
- PASS: Palette/Colors references are displayed only when returned by API/database keys.
- PASS: No Sprite-owned reusable color definitions were added.
- PASS: No page-local product data arrays were added.
- PASS: No browser storage product-data source of truth was added.
- PASS: No MEM DB, local-mem, fake-login, or silent fallback was introduced.
- PASS: No inline styles, style blocks, inline event handlers, or page-local CSS were added.
- PASS: Targeted Playwright coverage was added and passed.
- PASS: Required report artifacts were created.
- PASS: Repo-structured ZIP artifact was created under `tmp/`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# PR_26177_CHARLIE_011 Validation Lane

Status: PASS

## Commands

```powershell
git -c http.sslBackend=schannel fetch origin main
git rev-list --left-right --count origin/main...HEAD
```

Result: PASS, `0 0` before the PR branch edit work.

```powershell
rg -n "<style|style=|onclick=|onchange=|oninput=|onsubmit=|<script>" 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 the repository line-ending warning for `toolbox/sprites/index.html`.

Post-artifact note: a later `git diff --check` pass initially flagged trailing whitespace inside the generated `codex_review.diff` artifact. The generated artifact was normalized and the final check passed.

```powershell
node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list
```

Result: PASS, 3 passed.

## Playwright Coverage

Targeted Playwright coverage updated `docs_build/dev/reports/playwright_v8_coverage_report.txt` and recorded browser execution for `assets/toolbox/sprites/js/index.js`.
49 changes: 49 additions & 0 deletions docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# PR_26177_CHARLIE_011-sprites-tool-shell

Team: Charlie

Status: PASS

## Scope

Added the Sprites tool shell under the current Toolbox route using Theme V2 patterns. The page now presents a table-first sprite library surface, API-backed loading/empty/error states, and Palette/Colors reference messaging without implementing create/update/delete behavior.

## 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_011-sprites-tool-shell.md`
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-branch-validation.md`
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-requirements-checklist.md`
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-validation-lane.md`
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-manual-validation-notes.md`

## Implementation Notes

- The tool title remains `Sprites`.
- The legacy "Sprite Editor" framing was removed from this page.
- The shell uses existing Theme V2 classes and shared tool layout patterns.
- The page uses external JavaScript only through `assets/toolbox/sprites/js/index.js`.
- The browser calls `/api/sprites/records` and renders only data returned by the API contract.
- When the API is absent or unavailable, the page shows a visible unavailable state instead of fake records or a silent fallback.
- Palette/Colors remains the reusable color source of truth. Sprites displays Palette/Colors key references returned by the API and does not define reusable colors.

## Dependency Note

`PR_26177_CHARLIE_010-sprites-api-db-foundation` provides the concrete API/database foundation. Because that PR is not merged into `main` yet, this shell PR validates the UI against mocked API responses in Playwright and preserves a product-safe unavailable state for branches where the route is absent.

## Validation

- PASS: `git diff --check`
- PASS: inline CSS/script/handler scan for Sprites shell 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_011-sprites-tool-shell_delta.zip`
Loading