Skip to content

Commit 73753c8

Browse files
committed
Add Sprites tool shell
1 parent 1420bc8 commit 73753c8

11 files changed

Lines changed: 3340 additions & 1371 deletions

assets/toolbox/sprites/js/index.js

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
const SPRITES_API_PATH = "/api/sprites/records";
2+
3+
const elements = {
4+
apiStatus: document.querySelector("[data-sprites-api-status]"),
5+
count: document.querySelector("[data-sprites-count]"),
6+
emptyState: document.querySelector("[data-sprites-empty-state]"),
7+
errorState: document.querySelector("[data-sprites-error-state]"),
8+
libraryStatus: document.querySelector("[data-sprites-library-status]"),
9+
metadata: document.querySelector("[data-sprites-metadata]"),
10+
outputStatus: document.querySelector("[data-sprites-output-status]"),
11+
outputSummary: document.querySelector("[data-sprites-output-summary]"),
12+
paletteStatus: document.querySelector("[data-sprites-palette-status]"),
13+
refresh: document.querySelector("[data-sprites-refresh]"),
14+
tableBody: document.querySelector("[data-sprites-table-body]"),
15+
updated: document.querySelector("[data-sprites-updated]"),
16+
};
17+
18+
function setText(target, value) {
19+
if (target) {
20+
target.textContent = value;
21+
}
22+
}
23+
24+
function setHidden(target, hidden) {
25+
if (target) {
26+
target.hidden = hidden;
27+
}
28+
}
29+
30+
function createCell(value) {
31+
const cell = document.createElement("td");
32+
cell.textContent = value;
33+
return cell;
34+
}
35+
36+
function createHeaderCell(value) {
37+
const cell = document.createElement("th");
38+
cell.scope = "row";
39+
cell.textContent = value;
40+
return cell;
41+
}
42+
43+
function normalizeText(value, fallback = "Unavailable") {
44+
const text = String(value ?? "").trim();
45+
return text || fallback;
46+
}
47+
48+
function formatTimestamp(value) {
49+
const text = String(value ?? "").trim();
50+
if (!text) {
51+
return "Unavailable";
52+
}
53+
const date = new Date(text);
54+
if (Number.isNaN(date.getTime())) {
55+
return text;
56+
}
57+
return date.toLocaleString();
58+
}
59+
60+
function formatDimensions(sprite) {
61+
const width = Number(sprite?.width ?? sprite?.dimensions?.width);
62+
const height = Number(sprite?.height ?? sprite?.dimensions?.height);
63+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
64+
return "Unavailable";
65+
}
66+
return `${width} x ${height}`;
67+
}
68+
69+
function formatSource(sprite) {
70+
return normalizeText(sprite?.sourceName || sprite?.sourcePath || sprite?.storagePath || sprite?.storageKey || sprite?.sourceStorageReference);
71+
}
72+
73+
function paletteKeysFor(sprite) {
74+
if (Array.isArray(sprite?.paletteColorKeys)) {
75+
return sprite.paletteColorKeys.map((key) => String(key || "").trim()).filter(Boolean);
76+
}
77+
if (Array.isArray(sprite?.palette_color_keys)) {
78+
return sprite.palette_color_keys.map((key) => String(key || "").trim()).filter(Boolean);
79+
}
80+
return [];
81+
}
82+
83+
function usageCountFor(sprite) {
84+
const count = Number(sprite?.usageCount ?? sprite?.usage_count ?? sprite?.references?.length);
85+
return Number.isFinite(count) && count >= 0 ? String(count) : "0";
86+
}
87+
88+
function spriteRowsFromPayload(payload) {
89+
if (Array.isArray(payload?.data?.sprites)) {
90+
return payload.data.sprites;
91+
}
92+
if (Array.isArray(payload?.sprites)) {
93+
return payload.sprites;
94+
}
95+
return [];
96+
}
97+
98+
function renderLoading() {
99+
setText(elements.apiStatus, "Loading");
100+
setText(elements.libraryStatus, "Loading");
101+
setText(elements.outputStatus, "Loading");
102+
setText(elements.outputSummary, "Waiting for Sprites API response.");
103+
setText(elements.emptyState, "Loading Sprites records.");
104+
setText(elements.updated, "Checking");
105+
setHidden(elements.emptyState, false);
106+
setHidden(elements.errorState, true);
107+
if (elements.tableBody) {
108+
const row = document.createElement("tr");
109+
const cell = createCell("Loading Sprites records.");
110+
cell.colSpan = 8;
111+
row.append(cell);
112+
elements.tableBody.replaceChildren(row);
113+
}
114+
}
115+
116+
function renderUnavailable(message) {
117+
const detail = normalizeText(message, "Sprites API unavailable.");
118+
setText(elements.apiStatus, "Unavailable");
119+
setText(elements.libraryStatus, "Unavailable");
120+
setText(elements.outputStatus, "Unavailable");
121+
setText(elements.outputSummary, detail);
122+
setText(elements.emptyState, "Sprites records cannot be loaded from the API yet.");
123+
setText(elements.errorState, detail);
124+
setText(elements.metadata, "Sprite metadata unavailable until the Sprites API responds.");
125+
setText(elements.paletteStatus, "Palette/Colors references unavailable until Sprites records load from the API.");
126+
setText(elements.updated, new Date().toLocaleTimeString());
127+
setHidden(elements.emptyState, false);
128+
setHidden(elements.errorState, false);
129+
if (elements.tableBody) {
130+
const row = document.createElement("tr");
131+
const cell = createCell("Sprites API unavailable.");
132+
cell.colSpan = 8;
133+
row.append(cell);
134+
elements.tableBody.replaceChildren(row);
135+
}
136+
}
137+
138+
function renderPaletteStatus(sprites) {
139+
const referencedKeys = new Set();
140+
sprites.forEach((sprite) => {
141+
paletteKeysFor(sprite).forEach((key) => referencedKeys.add(key));
142+
});
143+
if (referencedKeys.size === 0) {
144+
setText(elements.paletteStatus, "No Palette/Colors references in current Sprites records.");
145+
return;
146+
}
147+
setText(elements.paletteStatus, `${referencedKeys.size} Palette/Colors key reference${referencedKeys.size === 1 ? "" : "s"} surfaced from API records.`);
148+
}
149+
150+
function renderRows(sprites) {
151+
if (!elements.tableBody) {
152+
return;
153+
}
154+
if (sprites.length === 0) {
155+
const row = document.createElement("tr");
156+
const cell = createCell("No Sprites records returned by the API.");
157+
cell.colSpan = 8;
158+
row.append(cell);
159+
elements.tableBody.replaceChildren(row);
160+
return;
161+
}
162+
163+
const rows = sprites.map((sprite) => {
164+
const row = document.createElement("tr");
165+
const paletteKeys = paletteKeysFor(sprite);
166+
row.dataset.spritesRowKey = normalizeText(sprite?.key, "");
167+
row.append(
168+
createHeaderCell(normalizeText(sprite?.name)),
169+
createCell(normalizeText(sprite?.status)),
170+
createCell(normalizeText(sprite?.category, "None")),
171+
createCell(formatSource(sprite)),
172+
createCell(formatDimensions(sprite)),
173+
createCell(paletteKeys.length ? paletteKeys.join(", ") : "None"),
174+
createCell(formatTimestamp(sprite?.updatedAt ?? sprite?.updated_at)),
175+
createCell(usageCountFor(sprite))
176+
);
177+
row.addEventListener("click", () => {
178+
const key = normalizeText(sprite?.key, "Unavailable");
179+
const mimeType = normalizeText(sprite?.mimeType ?? sprite?.mime_type, "Unavailable");
180+
const sizeBytes = normalizeText(sprite?.sizeBytes ?? sprite?.size_bytes, "Unavailable");
181+
setText(elements.metadata, `${normalizeText(sprite?.name)} (${key}) | ${mimeType} | ${formatDimensions(sprite)} | ${sizeBytes} bytes`);
182+
});
183+
return row;
184+
});
185+
elements.tableBody.replaceChildren(...rows);
186+
}
187+
188+
function renderSprites(payload) {
189+
const sprites = spriteRowsFromPayload(payload);
190+
const count = sprites.length;
191+
setText(elements.apiStatus, "Ready");
192+
setText(elements.libraryStatus, count > 0 ? "Ready" : "Empty");
193+
setText(elements.count, String(count));
194+
setText(elements.outputStatus, count > 0 ? "Ready" : "Empty");
195+
setText(elements.outputSummary, count > 0 ? `${count} sprite record${count === 1 ? "" : "s"} loaded from the API.` : "Sprites API responded with no records.");
196+
setText(elements.emptyState, count > 0 ? "" : "No Sprites records returned by the API.");
197+
setText(elements.updated, new Date().toLocaleTimeString());
198+
setText(elements.metadata, count > 0 ? "Select a sprite row to review its metadata." : "No sprite metadata available yet.");
199+
setHidden(elements.emptyState, count > 0);
200+
setHidden(elements.errorState, true);
201+
renderPaletteStatus(sprites);
202+
renderRows(sprites);
203+
}
204+
205+
async function loadSprites() {
206+
renderLoading();
207+
try {
208+
const response = await fetch(SPRITES_API_PATH, {
209+
cache: "no-store",
210+
headers: { accept: "application/json" },
211+
});
212+
let payload = null;
213+
try {
214+
payload = await response.json();
215+
} catch {
216+
payload = null;
217+
}
218+
if (!response.ok || payload?.ok === false) {
219+
const message = payload?.error?.message || payload?.message || `Sprites API returned ${response.status}.`;
220+
renderUnavailable(message);
221+
return;
222+
}
223+
renderSprites(payload || {});
224+
} catch (error) {
225+
renderUnavailable(error instanceof Error ? error.message : "Sprites API request failed.");
226+
}
227+
}
228+
229+
elements.refresh?.addEventListener("click", () => {
230+
void loadSprites();
231+
});
232+
233+
void loadSprites();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# PR_26177_CHARLIE_011 Branch Validation
2+
3+
Status: PASS
4+
5+
## Checks
6+
7+
- PASS: Started from `main`.
8+
- PASS: `main` was clean before the PR branch was created.
9+
- PASS: `main` and `origin/main` were synced at `0 0` before the PR branch was created.
10+
- PASS: Remote sync was refreshed with Git's Windows certificate backend after the default OpenSSL certificate store failed.
11+
- PASS: Current work branch is `PR_26177_CHARLIE_011-sprites-tool-shell`.
12+
- PASS: Branch contains only the Sprites shell PR scope.
13+
- PASS: No merge was performed.
14+
- PASS: No `start_of_day` path is changed.
15+
16+
## Notes
17+
18+
The branch intentionally contains uncommitted changes while this report is generated. Final clean state is verified after commit and push.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PR_26177_CHARLIE_011 Manual Validation Notes
2+
3+
Status: PASS
4+
5+
## Manual Review
6+
7+
- Reviewed the Sprites page markup for Theme V2 layout consistency with existing Toolbox tools.
8+
- Verified page copy uses `Sprites`, not `Sprite Editor`.
9+
- Verified the page presents Sprites as asset management, not image editing.
10+
- Verified the tool does not create or duplicate Palette/Colors records.
11+
- Verified the browser module renders only API response data and uses explicit unavailable states when the API route is missing.
12+
- Verified the refresh control re-runs the API read action.
13+
- Verified the table-first layout includes user-visible loading, empty, populated, and unavailable states through targeted Playwright tests.
14+
15+
## Manual Limitation
16+
17+
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.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# PR_26177_CHARLIE_011 Requirements Checklist
2+
3+
Status: PASS
4+
5+
- PASS: Added Sprites tool route/shell under the current Toolbox structure.
6+
- PASS: Tool title is `Sprites`.
7+
- PASS: Used current GFS shell/layout patterns.
8+
- PASS: Used Theme V2 classes and shared layout conventions.
9+
- PASS: Navigation entry already exists in the shared Toolbox menu; no duplicate menu item was added.
10+
- PASS: HTML uses external JavaScript only.
11+
- PASS: Added loading, empty, and error/unavailable surfaces.
12+
- PASS: Shell reads `/api/sprites/records` and does not own authoritative product data in the browser.
13+
- PASS: Missing API is shown as an explicit unavailable state.
14+
- PASS: Palette/Colors is documented in-page as the reusable color source of truth.
15+
- PASS: Palette/Colors references are displayed only when returned by API/database keys.
16+
- PASS: No Sprite-owned reusable color definitions were added.
17+
- PASS: No page-local product data arrays were added.
18+
- PASS: No browser storage product-data source of truth was added.
19+
- PASS: No MEM DB, local-mem, fake-login, or silent fallback was introduced.
20+
- PASS: No inline styles, style blocks, inline event handlers, or page-local CSS were added.
21+
- PASS: Targeted Playwright coverage was added and passed.
22+
- PASS: Required report artifacts were created.
23+
- PASS: Repo-structured ZIP artifact was created under `tmp/`.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# PR_26177_CHARLIE_011 Validation Lane
2+
3+
Status: PASS
4+
5+
## Commands
6+
7+
```powershell
8+
git -c http.sslBackend=schannel fetch origin main
9+
git rev-list --left-right --count origin/main...HEAD
10+
```
11+
12+
Result: PASS, `0 0` before the PR branch edit work.
13+
14+
```powershell
15+
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
16+
```
17+
18+
Result: PASS, no matches.
19+
20+
```powershell
21+
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
22+
```
23+
24+
Result: PASS, no matches.
25+
26+
```powershell
27+
git diff --check
28+
```
29+
30+
Result: PASS. Git reported only the repository line-ending warning for `toolbox/sprites/index.html`.
31+
32+
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.
33+
34+
```powershell
35+
node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list
36+
```
37+
38+
Result: PASS, 3 passed.
39+
40+
## Playwright Coverage
41+
42+
Targeted Playwright coverage updated `docs_build/dev/reports/playwright_v8_coverage_report.txt` and recorded browser execution for `assets/toolbox/sprites/js/index.js`.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# PR_26177_CHARLIE_011-sprites-tool-shell
2+
3+
Team: Charlie
4+
5+
Status: PASS
6+
7+
## Scope
8+
9+
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.
10+
11+
## Changed Files
12+
13+
- `toolbox/sprites/index.html`
14+
- `assets/toolbox/sprites/js/index.js`
15+
- `tests/playwright/tools/SpritesToolShell.spec.mjs`
16+
- `docs_build/dev/reports/playwright_v8_coverage_report.txt`
17+
- `docs_build/dev/reports/codex_review.diff`
18+
- `docs_build/dev/reports/codex_changed_files.txt`
19+
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell.md`
20+
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-branch-validation.md`
21+
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-requirements-checklist.md`
22+
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-validation-lane.md`
23+
- `docs_build/dev/reports/PR_26177_CHARLIE_011-sprites-tool-shell-manual-validation-notes.md`
24+
25+
## Implementation Notes
26+
27+
- The tool title remains `Sprites`.
28+
- The legacy "Sprite Editor" framing was removed from this page.
29+
- The shell uses existing Theme V2 classes and shared tool layout patterns.
30+
- The page uses external JavaScript only through `assets/toolbox/sprites/js/index.js`.
31+
- The browser calls `/api/sprites/records` and renders only data returned by the API contract.
32+
- When the API is absent or unavailable, the page shows a visible unavailable state instead of fake records or a silent fallback.
33+
- 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.
34+
35+
## Dependency Note
36+
37+
`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.
38+
39+
## Validation
40+
41+
- PASS: `git diff --check`
42+
- PASS: inline CSS/script/handler scan for Sprites shell files found no matches.
43+
- PASS: browser storage and forbidden local data pattern scan found no matches.
44+
- PASS: no `start_of_day` files changed.
45+
- PASS: `node ./node_modules/@playwright/test/cli.js test tests/playwright/tools/SpritesToolShell.spec.mjs --project=playwright --workers=1 --reporter=list`
46+
47+
## ZIP Artifact
48+
49+
- `tmp/PR_26177_CHARLIE_011-sprites-tool-shell_delta.zip`

0 commit comments

Comments
 (0)