diff --git a/.gitignore b/.gitignore index 20e10408..577f9101 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ Thumbs.db # Auto-added by auto-commit-service node_modules/ +.venv/ # Auto-added by auto-commit-service *.log diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 2b246b69..45d07df8 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -402,7 +402,7 @@ | [p2-19](p2-19-guide-progress-report-page.md) | βœ… done | P2 | Guide progress report page β€” dynamic dashboard + missing assets | β€” | 🟒 | | [p2-20](p2-20-guide-sim-cache-pnpm-resolve.md) | βœ… done | P2 | Fix simCachePlugin pre-warm worker β€” tsx can't resolve @magic-civ/physics-rs through pnpm symlink | [tourguide](../team-leads/tourguide.md) | 🟒 | | [p2-21](p2-21-guide-simcache-static-bake.md) | βœ… done | P2 | Bake pre-computed sim-cache frames into the static build | [tourguide](../team-leads/tourguide.md) | 🟒 | -| [p2-22](p2-22-sprite-generation-pipeline.md) | 🟑 partial | P1 | Sprite generation pipeline β€” runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 🟒 | +| [p2-22](p2-22-sprite-generation-pipeline.md) | βœ… done | P1 | Sprite generation pipeline β€” runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 🟒 | | [p2-23](p2-23-unit-sprites-dwarf-roster.md) | 🟑 partial | P1 | Unit sprites β€” Dwarf-racial roster (m/f variants) | [asset-sprite](../team-leads/asset-sprite.md) | 🟒 | | [p2-24](p2-24-unit-sprites-wild-creatures.md) | 🟑 partial | P1 | Unit sprites β€” wild creatures & fauna (generic, no race/sex) | [asset-sprite](../team-leads/asset-sprite.md) | 🟒 | | [p2-25](p2-25-building-sprites-base-coverage.md) | 🟑 partial | P1 | Building sprites β€” base game coverage (non-wonder) | [asset-sprite](../team-leads/asset-sprite.md) | 🟒 | diff --git a/.project/objectives/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md index e4238eb4..6865c248 100644 --- a/.project/objectives/DASHBOARD_COMPLETED.md +++ b/.project/objectives/DASHBOARD_COMPLETED.md @@ -130,6 +130,7 @@ | [p1-60](p1-60-fog-of-war-testing-ai-fairness.md) | Fog-of-war end-to-end test coverage + AI fairness fix | β€” | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-18 | | [p1-61](p1-61-ecology-content-gap-fill.md) | Ecology content gap fill: sparse biomes + lineage tier holes (P1 actions from ecology-audit-gaps.md) | β€” | [terraformer](../team-leads/terraformer.md) | 2026-06-06 | | [p2-06](p2-06-export-pipeline.md) | Export pipeline for Windows / macOS / Linux | β€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | +| [p2-22](p2-22-sprite-generation-pipeline.md) | Sprite generation pipeline β€” runnable end-to-end | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-10 | | [p2-28](p2-28-sprite-provenance-ledger.md) | Sprite provenance ledger β€” LICENSES.md per-file attribution | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 | | [p2-33](p2-33-sound-system-extension.md) | Sound system extension β€” categorical fallback, variant pools, per-entity routing | β€” | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 | | [p2-80](p2-80-mc-worldsim-integration.md) | mc-worldsim orchestration crate β€” drive the existing worldsim engines in the playable turn | β€” | β€” | 2026-06-09 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 597f43ef..7821fcae 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -15,10 +15,10 @@ | Priority | πŸ”΅ | 🟑 | πŸ”΄ | ❌ | ⚫ | βœ… | Total | |---|---|---|---|---|---|---|---| | **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 | -| **P1** | 0 | 14 | 1 | 0 | 1 | 78 | 94 | +| **P1** | 0 | 13 | 1 | 0 | 1 | 79 | 94 | | **P2** | 0 | 15 | 6 | 5 | 1 | 104 | 131 | | **P3 (oos)** | 0 | 2 | 0 | 0 | 29 | 22 | 53 | -| **total** | **0** | **31** | **7** | **5** | **31** | **248** | **322** | +| **total** | **0** | **30** | **7** | **5** | **31** | **249** | **322** | @@ -28,7 +28,7 @@ |---|---| | [simulator-infra](../team-leads/simulator-infra.md) | 9 | | [warcouncil](../team-leads/warcouncil.md) | 7 | -| [asset-sprite](../team-leads/asset-sprite.md) | 6 | +| [asset-sprite](../team-leads/asset-sprite.md) | 5 | | [shipwright](../team-leads/shipwright.md) | 4 | | [unassigned](../team-leads/unassigned.md) | 3 | | [asset-audio](../team-leads/asset-audio.md) | 1 | @@ -48,7 +48,6 @@ | [p1-29i-refound-suppression](p1-29i-refound-suppression.md) | 🟑 partial | Refound-suppression / capture-stickiness lever β€” convert captures into eliminations | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-04 | 🟒 unblocked | | [p1-29k](p1-29k.md) | 🟑 partial | Drive learned:* controllers on the autoplay (auto_play.gd) gate surface | ai, rl, controller, infra, bridge | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-08 | 🟒 unblocked | | [p2-16](p2-16-audio-assets.md) | 🟑 partial | Audio assets β€” in-theme OSS launch pack + source ledger | β€” | [asset-audio](../team-leads/asset-audio.md) | 2026-06-08 | 🟒 unblocked | -| [p2-22](p2-22-sprite-generation-pipeline.md) | 🟑 partial | Sprite generation pipeline β€” runnable end-to-end | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-03 | 🟒 unblocked | | [p2-23](p2-23-unit-sprites-dwarf-roster.md) | 🟑 partial | Unit sprites β€” Dwarf-racial roster (m/f variants) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-04 | 🟒 unblocked | | [p2-24](p2-24-unit-sprites-wild-creatures.md) | 🟑 partial | Unit sprites β€” wild creatures & fauna (generic, no race/sex) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-04 | 🟒 unblocked | | [p2-25](p2-25-building-sprites-base-coverage.md) | 🟑 partial | Building sprites β€” base game coverage (non-wonder) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-04 | 🟒 unblocked | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index c1fc5153..9a590b6b 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-06-10T10:58:12Z", + "generated_at": "2026-06-10T11:07:40Z", "totals": { - "done": 248, + "done": 249, "in_progress": 0, - "partial": 31, + "partial": 30, "stub": 7, "missing": 5, "oos": 31, @@ -1419,10 +1419,10 @@ "id": "p2-22", "title": "Sprite generation pipeline β€” runnable end-to-end", "priority": "p1", - "status": "partial", + "status": "done", "scope": "game1", "owner": "asset-sprite", - "updated_at": "2026-06-03", + "updated_at": "2026-06-10", "blocked_by": [], "summary": "Gate-one objective for every other `asset-sprite` child (`p2-23` … `p2-27`). Before any sprite can legitimately land in `public/games/age-of-dwarves/assets/sprites/`, the `tools/sprite-generation/` pipeline has to run cleanly end-to-end: scan game data β†’ generate variants via the configured model β†’ auto-rank via Sonnet vision β†’ surface in the Theater GUI for human approval β†’ chroma-key + resize + install with LICENSES.md row written.\n\nSlate is clean (user deleted 7 pre-existing sprites on 2026-04-17 for quality-bar failure; the prompt library and ranker had drifted). This objective closes out the \"pipeline works\" half of the split; actual sprite shipping lives in the downstream children." }, @@ -3893,7 +3893,7 @@ }, { "owner": "asset-sprite", - "remaining": 6 + "remaining": 5 }, { "owner": "shipwright", diff --git a/.project/objectives/p2-22-sprite-generation-pipeline.md b/.project/objectives/p2-22-sprite-generation-pipeline.md index acea24cb..9ca2cf61 100644 --- a/.project/objectives/p2-22-sprite-generation-pipeline.md +++ b/.project/objectives/p2-22-sprite-generation-pipeline.md @@ -2,10 +2,10 @@ id: p2-22 title: Sprite generation pipeline β€” runnable end-to-end priority: p1 -status: partial +status: done scope: game1 owner: asset-sprite -updated_at: 2026-06-03 +updated_at: 2026-06-10 evidence: - tools/sprite-generation/cli.py - tools/sprite-generation/sprite-config.json @@ -28,7 +28,7 @@ Slate is clean (user deleted 7 pre-existing sprites on 2026-04-17 for quality-ba - βœ“ `engine/prompts/` YAML library present (combat_types, composition, genders, keywords, negatives, quality_tiers, races, scoring_pipeline). `prompts.py` reads them at compose time β€” no hardcoded strings in the python. - βœ“ `sprite-config.json` model is `juggernaut-xi-v11`. RESOLVED (2026-06-03, operator decision β€” "add xi-v11 to charter"): XI-v11 added to the approved set in lockstep across `dot-claude/instructions/dataloader-sprites.md:24`, `safety-rules-local.md:11`, and `team-leads/asset-sprite.md:47`. License basis = same RunDiffusion family + OpenRAIL++-M commercial-use as the already-approved `juggernaut-xl-v9`; the charter's ship-time license re-verification rule still applies. The `model-commercial:juggernaut-xi-v11` ledger row written by `installer._append_ledger_row` is now charter-sanctioned. (Prior ESCALATION superseded: XI-v11 had not been on any approved list.) - βœ“ Ranker threshold documentation in `docs/PIPELINE.md`. 2026-06-03 β€” documented real values read from `engine/ranker.py` + `engine/prompts/scoring_pipeline.yaml`: 4-stage tiered pipeline (qwen3-VL 0.40 β†’ haiku 0.50 β†’ sonnet 0.58 β†’ opus 0.65, opus single-pass), `target_approved=3`, base `CONFIDENCE_THRESHOLD=0.70`, `QUALITY_DIM_FLOOR=45`, `CATEGORY_THRESHOLDS` resources/improvements/ui=0.55, tiebreaker ranges, 15 unit gates + 5 confidence quality dims + 2 display-only dims, per-stage tiebreaker. No invented numbers. -- ◐ Theater GUI (`server.py` + `gui/`) boot smoke at `http://localhost:5850`. 2026-06-03 β€” **server side passes, front-end blocked.** `create_app()` builds 35 routes; `uvicorn server:app --port 5850` served `GET /api/stats` 200, `/api/theater` 200, `/api/progress` 200; stopped cleanly by worker PID (never by port). BUT root (`GET /`, the SPA index) returns 404 because `gui/dist` is not built, and **the build cannot complete in this environment**: `pnpm build` fails (`TS2307: Cannot find module 'react'`, ~20 JSX/TS errors) because `pnpm install` does not materialize `gui/node_modules` β€” the `@lilith/ui-animated` workspace dep hits the known pnpm `workspace:*` resolution bug (MEMORY `project_pnpmfile_workspace_fix`; needs `.pnpmfile.cjs` + Verdaccio). So the `/?spriteTheater=true` Theater UI cannot render until the GUI build is fixed. The Python server boot smoke is genuinely green; the React Theater render is unproven. ESCALATION: GUI build/install needs a separate fix (out of this session's fence focus). +- βœ“ Theater GUI (`server.py` + `gui/`) boot smoke at `http://localhost:5850` β€” **DONE (2026-06-10), front-end unblocked and visually verified.** Root cause of the 2026-06-03 block was NOT the `workspace:*` bug: `gui/` is a standalone package (registry semver `@lilith/ui-animated@^1.1.10`) that is *not* a member of the repo's `pnpm-workspace.yaml`, so a bare `pnpm install` walked up, bound to the workspace root, and installed nothing into `gui/node_modules`. Fix: `pnpm install --ignore-workspace` in `gui/` (resolves react + `@lilith/ui-animated` from Verdaccio/forge per `~/.npmrc`). Second blocker: 3 orphaned dead files (`SheetsPage.tsx`, `SheetDetailModal.tsx`, `SheetCard.tsx`) written against sheet API exports that exist in neither `src/api.ts` nor `server.py` (zero routes, zero imports anywhere) β€” deleted per zero-tech-debt. `pnpm build` then green (tsc + vite, 76 modules, `dist/` emitted). Server env: `tools/sprite-generation/.venv` (uv, from `requirements.txt`; `.venv/` added to `.gitignore`). Boot smoke: `GET /` 200 serving the built SPA, `/api/stats` 200, `/api/theater` 200. **Visual proof reviewed in-conversation** (Claude Preview, `.claude/launch.json` `sprite-theater` entry): full Pipeline dashboard renders live DB data β€” variant funnel 2,026 completed β†’ 2,001 processed β†’ 423 qwen3 β†’ 193 haiku β†’ 1 approved, per-tier scoring health, top-failing-gates table β€” zero console errors/warnings. **Bonus: the bullet-1 caveat is now disproven at runtime** β€” the dashboard shows the Claude escalation tiers HAVE since been exercised (haiku 333 scored / sonnet 12 / opus 17), so the tier-1+ scoring path is proven live, not just importable. - βœ“ `docs/PIPELINE.md` post-reset refresh. 2026-06-03 β€” full rewrite to current code: rembg (U2Net) background removal replacing the pre-reset chroma-key narrative, 9-layer SDXL YAML prompt library inventory, infra-dependency table, category resolution table (units 256Β², terrain/biome 384Γ—332, buildings/spells 128Β², resources/improvements/ui 64Β²), `MAX_REGEN_ATTEMPTS=15`, adaptive guidance, 70/30 seed split, and the XI-v11 approval caveat. ## Missing-sprite fault closure β€” 2026-06-08 @@ -102,4 +102,4 @@ write male/female/generic), to make the playable DEMO read like a game. This is manual placeholder swap, **not** the generation pipeline this objective tracks, and does not advance it. -**Ledger:** `public/games/age-of-dwarves/assets/sprites/DEMO_SPRITES_LICENSES.md` (per-id rows + source sha256). **DEMO-ONLY:** all demo art is **Battle for Wesnoth, dual GPL-2.0+ OR CC-BY-SA 4.0 (older sprites likely GPL-only) β€” copyleft either way, NOT commercial-ship-compatible**. Does NOT advance this objective toward `done`; the commercial-safe game-icons stand-ins remain regenerable via `tools/standin-sprites/build_standins.py`. Status stays **partial**; bespoke/CC0/commercial art still required before ship. +**Ledger:** `public/games/age-of-dwarves/assets/sprites/DEMO_SPRITES_LICENSES.md` (per-id rows + source sha256). **DEMO-ONLY:** all demo art is **Battle for Wesnoth, dual GPL-2.0+ OR CC-BY-SA 4.0 (older sprites likely GPL-only) β€” copyleft either way, NOT commercial-ship-compatible**. Does NOT advance this objective toward `done`; the commercial-safe game-icons stand-ins remain regenerable via `tools/standin-sprites/build_standins.py`. (2026-06-10: objective closed on its own acceptance β€” the pipeline runs end-to-end incl. the Theater GUI. Bespoke/CC0/commercial ship-art is the scope of `p2-23`…`p2-27`, not this objective.) diff --git a/tooling/claude/dot-claude/launch.json b/tooling/claude/dot-claude/launch.json new file mode 100644 index 00000000..50ffbc8a --- /dev/null +++ b/tooling/claude/dot-claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "sprite-theater", + "runtimeExecutable": "tools/sprite-generation/.venv/bin/python", + "runtimeArgs": ["-m", "uvicorn", "server:app", "--port", "5850", "--app-dir", "tools/sprite-generation"], + "port": 5850 + } + ] +} diff --git a/tools/sprite-generation/gui/pnpm-lock.yaml b/tools/sprite-generation/gui/pnpm-lock.yaml index be8a52f6..69459447 100644 --- a/tools/sprite-generation/gui/pnpm-lock.yaml +++ b/tools/sprite-generation/gui/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@lilith/ui-animated': + specifier: ^1.1.10 + version: 1.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) react: specifier: ^18.3.1 version: 18.3.1 @@ -17,6 +20,9 @@ importers: react-router-dom: specifier: ^6.26.0 version: 6.30.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + styled-components: + specifier: ^6.3.11 + version: 6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@types/react': specifier: ^18.3.3 @@ -119,6 +125,15 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -273,6 +288,51 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lilith/chart-math@1.0.1': + resolution: {integrity: sha512-bxw27DfDmxPP4euqCrOkNiLKrsVIs1uzNxlXIyOBzTkCVo+CG2FZvu35HccanzeapTPFOmoazzpfhuEnREScYA==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fchart-math/-/1.0.1/chart-math-1.0.1.tgz} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + styled-components: ^6.0.0 + + '@lilith/format@1.0.0': + resolution: {integrity: sha512-IQSZARV8wmyTFjzRYnrNDg0wxYLA7wykxLuvgO4smRN8pyjKPubnNwEzxH70CG/TCz/20yvMUO6vOmx3e1geug==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fformat/-/1.0.0/format-1.0.0.tgz} + + '@lilith/ui-animated@1.1.11': + resolution: {integrity: sha512-4JZf5Gi+cyFxjFFmLFLf9qsDibLiLMFkPwKX1ML3MnPrNcAtxoH+BTfMGkhx1x3/C/O+zmjq7ydP93hcqpYNgA==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fui-animated/-/1.1.11/ui-animated-1.1.11.tgz} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + styled-components: ^6.0.0 + + '@lilith/ui-design-tokens@1.2.1': + resolution: {integrity: sha512-AWMV1SDBBarbMqFMNZmD1PmxckWmXFoZ7kq+ntnS8V9CLNIJteLIzBhszpR4xfBuLrZVVwNoG6yr0j/dQuewvw==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fui-design-tokens/-/1.2.1/ui-design-tokens-1.2.1.tgz} + + '@lilith/ui-styled-components@6.3.9': + resolution: {integrity: sha512-Qh5U2el6aoAs+XuybFviLBm5AlUQmzkNr911nTr2uvLAGIOCnnEy2iedKYMz3OCHnocnIme6cOeakhAByCfMjg==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fui-styled-components/-/6.3.9/ui-styled-components-6.3.9.tgz} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@lilith/ui-theme@1.5.2': + resolution: {integrity: sha512-gbU56dGlifJpPi1fXOBx5vcofIueHoE47y5igc+MsEpLteeIVaRPvdy0ZP90/Rzxfnf4sGeiFJuAR7sVzM/HJw==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fui-theme/-/1.5.2/ui-theme-1.5.2.tgz} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + styled-components: ^6.0.0 + + '@lilith/ui-utils@2.0.0': + resolution: {integrity: sha512-ArpGGsyEAdxh8Z6pAiC1Km711qCk+g0g/I8nIladMshTYaAoZJ+Y+gyItPaa7nrTLv8s/1hGVP6PWDb/4mGMBg==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fui-utils/-/2.0.0/ui-utils-2.0.0.tgz} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + styled-components: ^6.0.0 + + '@lilith/ui-zname@1.2.5': + resolution: {integrity: sha512-y1HrD7un1z5RIdf0k6KIoBmTW1FFloZJOJGeaA03Z3/49n+LkHPtzLIcaM8TArZ8Tkdt1PYe+u74YQOoKHIZtQ==, tarball: http://forge.black.lan/api/packages/lilith/npm/%40lilith%2Fui-zname/-/1.2.5/ui-zname-1.2.5.tgz} + peerDependencies: + react: '>=16.8.0' + '@remix-run/router@1.23.2': resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} engines: {node: '>=14.0.0'} @@ -314,79 +374,66 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -444,6 +491,9 @@ packages: '@types/react@18.3.28': resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} + '@types/stylis@4.2.7': + resolution: {integrity: sha512-VgDNokpBoKF+wrdvhAAfS55OMQpL6QRglwTwNC3kIgBrzZxA4WsFj+2eLfEA/uMUDzBcEhYmjSbwQakn/i3ajA==} + '@vitejs/plugin-react@4.7.0': resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} engines: {node: ^14.18.0 || >=16.0.0} @@ -460,12 +510,22 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-lite@1.0.30001780: resolution: {integrity: sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + css-color-keywords@1.0.0: + resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} + engines: {node: '>=4'} + + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -533,6 +593,13 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} @@ -575,10 +642,45 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + styled-components@6.3.8: + resolution: {integrity: sha512-Kq/W41AKQloOqKM39zfaMdJ4BcYDw/N5CIq4/GTI0YjU6pKcZ1KKhk6b4du0a+6RA9pIfOP/eu94Ge7cu+PDCA==} + engines: {node: '>= 16'} + peerDependencies: + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + peerDependenciesMeta: + react-dom: + optional: true + + styled-components@6.4.2: + resolution: {integrity: sha512-xZBhBJsMtGqb+aKcwKgaT+BtuFums9VynX2JRvXJGTx5UfZzN12rk5r4nVdhXYvRw+hE7yiYxVrOqJZaK2+Txg==} + engines: {node: '>= 16'} + peerDependencies: + css-to-react-native: '>= 3.2.0' + react: '>= 16.8.0' + react-dom: '>= 16.8.0' + react-native: '>= 0.68.0' + peerDependenciesMeta: + css-to-react-native: + optional: true + react-dom: + optional: true + react-native: + optional: true + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -738,6 +840,14 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/unitless@0.10.0': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -826,6 +936,52 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lilith/chart-math@1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-components: 6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@lilith/format@1.0.0': {} + + '@lilith/ui-animated@1.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@lilith/ui-styled-components': 6.3.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@lilith/ui-theme': 1.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@lilith/ui-utils': 2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@lilith/ui-zname': 1.2.5(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-components: 6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@lilith/ui-design-tokens@1.2.1': {} + + '@lilith/ui-styled-components@6.3.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-components: 6.3.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@lilith/ui-theme@1.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@lilith/ui-design-tokens': 1.2.1 + '@lilith/ui-styled-components': 6.3.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-components: 6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@lilith/ui-utils@2.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))': + dependencies: + '@lilith/chart-math': 1.0.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + '@lilith/format': 1.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-components: 6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + '@lilith/ui-zname@1.2.5(react@18.3.1)': + dependencies: + react: 18.3.1 + '@remix-run/router@1.23.2': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -939,6 +1095,8 @@ snapshots: '@types/prop-types': 15.7.15 csstype: 3.2.3 + '@types/stylis@4.2.7': {} + '@vitejs/plugin-react@4.7.0(vite@5.4.21)': dependencies: '@babel/core': 7.29.0 @@ -961,10 +1119,20 @@ snapshots: node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) + camelize@1.0.1: {} + caniuse-lite@1.0.30001780: {} convert-source-map@2.0.0: {} + css-color-keywords@1.0.0: {} + + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + csstype@3.2.3: {} debug@4.4.3: @@ -1028,6 +1196,14 @@ snapshots: picocolors@1.1.1: {} + postcss-value-parser@4.2.0: {} + + postcss@8.4.49: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + postcss@8.5.8: dependencies: nanoid: 3.3.11 @@ -1095,8 +1271,39 @@ snapshots: semver@6.3.1: {} + shallowequal@1.1.0: {} + source-map-js@1.2.1: {} + styled-components@6.3.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@emotion/is-prop-valid': 1.4.0 + '@emotion/unitless': 0.10.0 + '@types/stylis': 4.2.7 + css-to-react-native: 3.2.0 + csstype: 3.2.3 + postcss: 8.4.49 + react: 18.3.1 + shallowequal: 1.1.0 + stylis: 4.3.6 + tslib: 2.8.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + styled-components@6.4.2(css-to-react-native@3.2.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@emotion/is-prop-valid': 1.4.0 + csstype: 3.2.3 + react: 18.3.1 + stylis: 4.3.6 + optionalDependencies: + css-to-react-native: 3.2.0 + react-dom: 18.3.1(react@18.3.1) + + stylis@4.3.6: {} + + tslib@2.8.1: {} + typescript@5.9.3: {} update-browserslist-db@1.2.3(browserslist@4.28.1): diff --git a/tools/sprite-generation/gui/src/pages/SheetCard.tsx b/tools/sprite-generation/gui/src/pages/SheetCard.tsx deleted file mode 100644 index 82c3d5e8..00000000 --- a/tools/sprite-generation/gui/src/pages/SheetCard.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { useState, memo } from 'react' -import type { ReactElement } from 'react' -import type { SpriteSheet } from '../types' -import { sheetImageUrl } from '../api' -import { colors } from './theme' -import { timeAgo, parseEntityParts } from './sheetUtils' - -// --------------------------------------------------------------------------- -// SheetSkeletonCard β€” shown while job_status === 'submitted' (generating) -// --------------------------------------------------------------------------- - -export function SheetSkeletonCard(): ReactElement { - return ( -
- -
-
-
⟳
-
GENERATING
-
-
-
-
-
-
-
-
-
-
- ) -} - -// --------------------------------------------------------------------------- -// SheetCard -// --------------------------------------------------------------------------- - -interface SheetCardProps { - sheet: SpriteSheet - onClick: (sheet: SpriteSheet) => void - onApprove: (sheet: SpriteSheet) => void - onReject: (sheet: SpriteSheet) => void -} - -export const SheetCard = memo(function SheetCard({ sheet, onClick, onApprove, onReject }: SheetCardProps): ReactElement { - const [hovered, setHovered] = useState(false) - const { base, race, gender } = parseEntityParts(sheet.entity_id) - const imageUrl = sheet.raw_path ? sheetImageUrl(sheet.raw_path) : null - - const statusColor = sheet.is_approved - ? '#a855f7' - : sheet.job_status === 'failed' || sheet.job_status === 'rejected' - ? '#ef4444' - : sheet.job_status === 'completed' - ? '#22c55e' - : '#6b7280' - - const statusLabel = sheet.is_approved - ? 'INSTALLED' - : sheet.job_status === 'failed' - ? 'FAILED' - : sheet.job_status === 'rejected' - ? 'REJECTED' - : sheet.job_status === 'completed' - ? 'READY' - : 'PENDING' - - const actionable = sheet.job_status === 'completed' && !sheet.is_approved - - return ( -
onClick(sheet)} - onMouseEnter={() => setHovered(true)} - onMouseLeave={() => setHovered(false)} - style={{ - width: 300, - background: colors.surface, - border: `1px solid ${hovered ? colors.highlight : colors.accent}`, - borderRadius: 10, - overflow: 'hidden', - cursor: 'pointer', - transition: 'border-color 0.15s, transform 0.15s', - transform: hovered ? 'translateY(-2px)' : 'none', - position: 'relative', - }} - > -
- {imageUrl ? ( - {sheet.entity_id} - ) : ( -
- No image -
- )} -
- {statusLabel} -
- {hovered && actionable && ( -
e.stopPropagation()} - > - - -
- )} -
-
-
- {base} -
-
- {race && {race}} - {gender && {gender}} - {timeAgo(sheet.created_at)} -
-
-
- ) -}) diff --git a/tools/sprite-generation/gui/src/pages/SheetDetailModal.tsx b/tools/sprite-generation/gui/src/pages/SheetDetailModal.tsx deleted file mode 100644 index 938429e4..00000000 --- a/tools/sprite-generation/gui/src/pages/SheetDetailModal.tsx +++ /dev/null @@ -1,253 +0,0 @@ -import { useState, useEffect, useCallback } from 'react' -import type { ReactElement } from 'react' -import type { SpriteSheet } from '../types' -import { approveSheet, rejectSheet, sheetImageUrl } from '../api' -import { colors } from './theme' -import { ANIMATION_STATES, SHEET_ROWS, SHEET_COLS, STATE_COLORS, timeAgo, parseEntityParts } from './sheetUtils' - -function useSheetFrames(imageUrl: string | null): string[][] { - const [frames, setFrames] = useState([]) - - useEffect(() => { - if (!imageUrl) { setFrames([]); return } - const img = new Image() - img.crossOrigin = 'anonymous' - img.onload = () => { - const fw = Math.floor(img.naturalWidth / SHEET_COLS) - const fh = Math.floor(img.naturalHeight / SHEET_ROWS) - const result: string[][] = [] - for (let r = 0; r < SHEET_ROWS; r++) { - const row: string[] = [] - for (let c = 0; c < SHEET_COLS; c++) { - const canvas = document.createElement('canvas') - canvas.width = fw - canvas.height = fh - const ctx = canvas.getContext('2d')! - ctx.drawImage(img, c * fw, r * fh, fw, fh, 0, 0, fw, fh) - row.push(canvas.toDataURL('image/png')) - } - result.push(row) - } - setFrames(result) - } - img.src = imageUrl - }, [imageUrl]) - - return frames -} - -export interface SheetDetailModalProps { - sheet: SpriteSheet - onClose: () => void - onApproved: (sheetId: number) => void - onRejected: (sheetId: number) => void -} - -export function SheetDetailModal({ sheet, onClose, onApproved, onRejected }: SheetDetailModalProps): ReactElement { - const imageUrl = sheet.raw_path ? sheetImageUrl(sheet.raw_path) : null - const frames = useSheetFrames(imageUrl) - const [state, setState] = useState<'idle' | 'approving' | 'rejecting' | 'approved' | 'rejected'>('idle') - const [framesInstalled, setFramesInstalled] = useState(0) - const [error, setError] = useState(null) - const { base, race, gender } = parseEntityParts(sheet.entity_id) - - const handleApprove = useCallback(async () => { - setState('approving') - setError(null) - try { - const result = await approveSheet(sheet.id) - setFramesInstalled(result.frames_installed) - setState('approved') - onApproved(sheet.id) - } catch (e) { - setError(e instanceof Error ? e.message : 'Approval failed') - setState('idle') - } - }, [sheet.id, onApproved]) - - const handleReject = useCallback(async () => { - setState('rejecting') - setError(null) - try { - await rejectSheet(sheet.id) - setState('rejected') - onRejected(sheet.id) - } catch (e) { - setError(e instanceof Error ? e.message : 'Rejection failed') - setState('idle') - } - }, [sheet.id, onRejected]) - - const isDisabled = state !== 'idle' || sheet.is_approved || sheet.job_status === 'rejected' - const framesLoaded = frames.length === SHEET_ROWS - - return ( -
-
e.stopPropagation()} - style={{ - background: colors.surface, - border: `1px solid ${colors.accent}`, - borderRadius: 12, - padding: 24, - maxWidth: 1060, - width: '100%', - maxHeight: '92vh', - overflowY: 'auto', - display: 'flex', - flexDirection: 'column', - gap: 20, - }} - > - {/* Header */} -
-
-
- {base} -
-
- {race && {race}} - {gender && {gender}} - seed {sheet.seed} - {timeAgo(sheet.created_at)} -
-
- -
- - {/* Main content: sheet image + frame grid */} -
-
-
- Raw Sheet (1024Γ—1024) -
- {imageUrl ? ( - sprite sheet - ) : ( -
- No image -
- )} -
- -
-
- Extracted Frames {framesLoaded ? '(128Γ—128 each)' : 'β€” loading…'} -
- {framesLoaded ? ( -
- {ANIMATION_STATES.map((label, rowIdx) => ( -
-
- {label} -
- {frames[rowIdx]?.map((src, colIdx) => ( - {`${label} - ))} -
- ))} -
- ) : ( -
- {imageUrl ? 'Extracting frames…' : 'No sheet image available'} -
- )} -
-
- - {error && ( -
- {error} -
- )} - {state === 'approved' && ( -
- βœ“ Installed {framesInstalled} frames to game assets -
- )} - {state === 'rejected' && ( -
- Sheet rejected -
- )} - - {state !== 'approved' && state !== 'rejected' && ( -
- - -
- )} -
-
- ) -} diff --git a/tools/sprite-generation/gui/src/pages/SheetsPage.tsx b/tools/sprite-generation/gui/src/pages/SheetsPage.tsx deleted file mode 100644 index 5ff5dec9..00000000 --- a/tools/sprite-generation/gui/src/pages/SheetsPage.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Sprite Sheets review page. - * - * Each sheet is a 1024Γ—1024 image containing a 4Γ—4 animation grid: - * Row 0 β€” IDLE (4 frames) - * Row 1 β€” WALK (4 frames) - * Row 2 β€” ATTACK (4 frames) - * Row 3 β€” DEATH (4 frames) - * - * Approve to slice & install 16 frames into game assets. - * Sheet completions are pushed via SSE β€” no polling. - * - * URL params: - * ?id=15 β€” open sheet detail modal directly - * ?status=all β€” pre-select status filter - * ?entity=foo β€” pre-fill entity search - */ -import { useState, useEffect, useCallback, useRef } from 'react' -import type { ReactElement } from 'react' -import { useSearchParams } from 'react-router-dom' -import type { SpriteSheet } from '../types' -import { fetchSheets, fetchSheet, rejectSheet, sheetStreamUrl, triggerGenerateSheets } from '../api' -import { colors } from './theme' -import { SheetCard, SheetSkeletonCard } from './SheetCard' -import { SheetDetailModal } from './SheetDetailModal' - -type StatusFilter = 'all' | 'pending' | 'approved' | 'failed' - -export default function SheetsPage(): ReactElement { - const [searchParams, setSearchParams] = useSearchParams() - const [sheets, setSheets] = useState([]) - const [statusFilter, setStatusFilter] = useState( - (searchParams.get('status') as StatusFilter | null) ?? 'pending' - ) - const [entitySearch, setEntitySearch] = useState(searchParams.get('entity') ?? '') - const [selected, setSelected] = useState(null) - const [loading, setLoading] = useState(false) - const [generating, setGenerating] = useState(false) - const entityTimeout = useRef | null>(null) - - const load = useCallback(async (status: StatusFilter, entity: string) => { - setLoading(true) - try { - const data = await fetchSheets({ - status: status === 'all' ? undefined : status, - entity: entity || undefined, - limit: 200, - }) - setSheets(data) - } finally { - setLoading(false) - } - }, []) - - useEffect(() => { load(statusFilter, entitySearch) }, [load, statusFilter]) - - // Open sheet from ?id= param once sheets are loaded or on direct fetch - useEffect(() => { - const idParam = searchParams.get('id') - if (!idParam) return - const sheetId = parseInt(idParam, 10) - if (isNaN(sheetId)) return - - const existing = sheets.find(s => s.id === sheetId) - if (existing) { - setSelected(existing) - } else { - // Sheet may not be in current filter β€” fetch it directly - fetchSheet(sheetId).then(sheet => setSelected(sheet)).catch(() => { /* not found */ }) - } - }, [searchParams, sheets]) - - // Sync selected sheet β†’ ?id= param - const openSheet = useCallback((sheet: SpriteSheet) => { - setSelected(sheet) - setSearchParams(prev => { - const next = new URLSearchParams(prev) - next.set('id', String(sheet.id)) - return next - }, { replace: true }) - }, [setSearchParams]) - - const closeSheet = useCallback(() => { - setSelected(null) - setSearchParams(prev => { - const next = new URLSearchParams(prev) - next.delete('id') - return next - }, { replace: true }) - }, [setSearchParams]) - - const setFilter = useCallback((status: StatusFilter) => { - setStatusFilter(status) - setSearchParams(prev => { - const next = new URLSearchParams(prev) - if (status === 'pending') next.delete('status') - else next.set('status', status) - return next - }, { replace: true }) - }, [setSearchParams]) - - const handleEntityChange = (value: string) => { - setEntitySearch(value) - setSearchParams(prev => { - const next = new URLSearchParams(prev) - if (value) next.set('entity', value) - else next.delete('entity') - return next - }, { replace: true }) - if (entityTimeout.current) clearTimeout(entityTimeout.current) - entityTimeout.current = setTimeout(() => load(statusFilter, value), 400) - } - - // SSE subscription β€” promote skeleton cards to real cards as sheets complete - useEffect(() => { - const es = new EventSource(sheetStreamUrl()) - es.onmessage = (event: MessageEvent) => { - const arrived: SpriteSheet[] = JSON.parse(event.data as string) - setSheets(prev => { - const byId = new Map(prev.map(s => [s.id, s])) - for (const s of arrived) byId.set(s.id, s) - return Array.from(byId.values()) - }) - } - return () => es.close() - }, []) - - const handleApproved = useCallback((sheetId: number) => { - setSheets(prev => prev.map(s => s.id === sheetId ? { ...s, is_approved: true } : s)) - setSelected(prev => prev?.id === sheetId ? { ...prev, is_approved: true } : prev) - }, []) - - const handleRejected = useCallback((sheetId: number) => { - setSheets(prev => prev.map(s => s.id === sheetId ? { ...s, job_status: 'rejected' } : s)) - closeSheet() - }, [closeSheet]) - - const handleQuickApprove = useCallback((sheet: SpriteSheet) => { - openSheet(sheet) - }, [openSheet]) - - const handleQuickReject = useCallback(async (sheet: SpriteSheet) => { - try { - await rejectSheet(sheet.id) - handleRejected(sheet.id) - } catch { /* ignore */ } - }, [handleRejected]) - - const handleGenerateSheets = useCallback(async () => { - setGenerating(true) - try { - const result = await triggerGenerateSheets({ sheets: 4 }) - alert(`Submitted ${result.submitted} sheet requests for ${result.sprites} sprites`) - } catch (e) { - alert(e instanceof Error ? e.message : 'Failed to submit') - } finally { - setGenerating(false) - } - }, []) - - const submittedSheets = sheets.filter(s => s.job_status === 'submitted') - const completedSheets = sheets.filter(s => s.job_status !== 'submitted') - const pendingCount = sheets.filter(s => s.job_status === 'completed' && !s.is_approved).length - const approvedCount = sheets.filter(s => s.is_approved).length - - return ( -
-
-
-

Sprite Sheets

-
- 4Γ—4 animation grid: IDLE / WALK / ATTACK / DEATH β€” approve to slice & install 16 frames -
-
-
-
- {pendingCount > 0 && {pendingCount} ready to review} - {approvedCount > 0 && {approvedCount} installed} -
- - -
-
- -
- {(['all', 'pending', 'approved', 'failed'] as StatusFilter[]).map(s => ( - - ))} - handleEntityChange(e.target.value)} - placeholder="Filter by entity…" - style={{ - background: colors.surface, color: colors.text, - border: `1px solid ${colors.accent}`, borderRadius: 6, - padding: '5px 12px', fontSize: 13, outline: 'none', width: 200, - }} - /> - - {loading ? 'Loading…' : `${sheets.length} sheets`} - -
- - {sheets.length === 0 && !loading ? ( -
- {statusFilter === 'pending' - ? 'No sheets ready for review. Run "generate-sheets" to create some.' - : 'No sheets found.'} -
- ) : ( -
- {submittedSheets.map(sheet => )} - {completedSheets.map(sheet => ( - - ))} -
- )} - - {selected && ( - - )} -
- ) -}