From 51b02252320da07047c9996b11de029dac41dcdd Mon Sep 17 00:00:00 2001 From: Charlie Team <97194984+ToolboxAid@users.noreply.github.com> Date: Sun, 28 Jun 2026 12:53:20 -0400 Subject: [PATCH] Add Sprite Creator preview export --- assets/theme-v2/css/gamefoundrystudio.css | 18 + assets/toolbox/sprites/js/index.js | 75 ++++ .../tools/SpritesToolShell.spec.mjs | 12 + ...6179_CHARLIE_027-sprites-preview-export.md | 62 +++ .../dev/reports/codex_changed_files.txt | 2 +- docs_build/dev/reports/codex_review.diff | 355 ++++++++---------- toolbox/sprites/index.html | 11 + 7 files changed, 336 insertions(+), 199 deletions(-) create mode 100644 docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md diff --git a/assets/theme-v2/css/gamefoundrystudio.css b/assets/theme-v2/css/gamefoundrystudio.css index 22fc1de11..82dff717c 100644 --- a/assets/theme-v2/css/gamefoundrystudio.css +++ b/assets/theme-v2/css/gamefoundrystudio.css @@ -1239,6 +1239,24 @@ body.tool-focus-mode .tool-column:last-of-type { box-shadow: 0 1px 4px var(--swatch-shadow-color) } +.sprite-preview-shell { + display: flex; + justify-content: center; + padding: 12px; + border: 1px solid var(--line); + border-radius: var(--radius-md); + background: var(--panel-soft) +} + +.sprite-preview-canvas { + width: min(100%, 160px); + height: auto; + aspect-ratio: 1; + border: 1px solid var(--line); + background: var(--card-background); + image-rendering: pixelated +} + @media(max-width:980px) { .grid.cols-4, diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js index f332496d1..9db241a60 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js @@ -2,6 +2,13 @@ const DEFAULT_GRID_SIZE = 16; const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]); const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]); const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]); +const EDITOR_COLOR_CSS_VARIABLES = Object.freeze({ + blue: "--electric-blue", + gold: "--forge-gold", + green: "--green", + ink: "--text", + orange: "--molten-orange", +}); const editorState = { activeTool: "pencil", @@ -51,6 +58,7 @@ function updateDraftStatus() { if (status) { status.textContent = draftStatusText(); } + renderPreview(); } function updatePaletteStatus() { @@ -114,6 +122,64 @@ function fillGrid() { updateDraftStatus(); } +function editorColorValue(colorKey) { + const variableName = EDITOR_COLOR_CSS_VARIABLES[normalizeColorKey(colorKey)]; + const value = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim(); + return value || "#111111"; +} + +function renderPreview() { + const canvas = document.querySelector("[data-sprites-preview-canvas]"); + if (!canvas) { + return; + } + const context = canvas.getContext("2d"); + if (!context) { + return; + } + const size = editorState.gridSize; + const cellSize = canvas.width / size; + context.clearRect(0, 0, canvas.width, canvas.height); + for (const [key, colorKey] of editorState.paintedPixels.entries()) { + const [rowText, columnText] = key.split(":"); + const row = Number(rowText); + const column = Number(columnText); + if (!Number.isFinite(row) || !Number.isFinite(column)) { + continue; + } + context.fillStyle = editorColorValue(colorKey); + context.fillRect((column - 1) * cellSize, (row - 1) * cellSize, cellSize, cellSize); + } +} + +function exportPreviewPng() { + const canvas = document.querySelector("[data-sprites-preview-canvas]"); + const status = document.querySelector("[data-sprites-export-status]"); + if (!canvas) { + return; + } + canvas.toBlob((blob) => { + if (!blob) { + if (status) { + status.textContent = "PNG export is unavailable in this browser session."; + } + return; + } + const objectUrl = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = objectUrl; + link.download = "sprite-creator-draft.png"; + link.rel = "noopener"; + document.body.append(link); + link.click(); + link.remove(); + URL.revokeObjectURL(objectUrl); + if (status) { + status.textContent = "PNG downloaded from unsaved editor draft."; + } + }, "image/png"); +} + function setGridSize(size) { const grid = document.querySelector("[data-sprites-pixel-grid]"); const status = document.querySelector("[data-sprites-grid-status]"); @@ -186,9 +252,18 @@ function wirePaletteButtons() { }); } +function wireExportButton() { + const button = document.querySelector("[data-sprites-export-png]"); + if (button) { + button.addEventListener("click", exportPreviewPng); + } +} + wireGridControls(); wireDrawingTools(); wirePaletteButtons(); +wireExportButton(); setGridSize(DEFAULT_GRID_SIZE); setActiveTool(editorState.activeTool); setActiveColor(editorState.activeColor); +renderPreview(); diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs index 2e1825bf2..af9c022c7 100644 --- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs @@ -260,6 +260,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024); await expect(page.locator("[data-sprites-pixel-grid] .sprite-canvas-cell--blue")).toHaveCount(1024); await expect(page.locator("[data-sprites-draft-status]")).toContainText("1024 draft pixels painted"); + await expect(page.locator("[data-sprites-preview-canvas]")).toBeVisible(); + const previewHasPaint = await page.locator("[data-sprites-preview-canvas]").evaluate((canvas) => { + const context = canvas.getContext("2d"); + const pixel = context.getImageData(1, 1, 1, 1).data; + return pixel[3] > 0; + }); + expect(previewHasPaint).toBe(true); + const downloadPromise = page.waitForEvent("download"); + await page.getByRole("button", { name: "Download PNG" }).click(); + const download = await downloadPromise; + expect(download.suggestedFilename()).toBe("sprite-creator-draft.png"); + await expect(page.locator("[data-sprites-export-status]")).toContainText("PNG downloaded"); await expect(page.locator("[data-sprites-shell-status]")).toContainText("Editor ready"); await expect(page.locator("main")).toContainText("Palette/Colors remains the reusable color source"); await expect(page.locator("main")).not.toContainText(/Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation/i); diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md b/docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md new file mode 100644 index 000000000..f1a8fa62f --- /dev/null +++ b/docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md @@ -0,0 +1,62 @@ +# PR_26179_CHARLIE_027-sprites-preview-export + +Team: CHARLIE +Workflow: stacked feature workflow +Base branch: PR_26179_CHARLIE_026-sprites-palette-panel +Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_027-sprites-preview-export_delta.zip + +## Summary + +Added live preview and PNG download/export for the unsaved Sprite Creator editor draft. Export uses canvas `toBlob` and browser download behavior. This PR does not save to library, publish, write browser storage, or add API/DB/schema changes. + +## Branch Validation + +PASS + +- Current branch: PR_26179_CHARLIE_027-sprites-preview-export +- Based on: PR_26179_CHARLIE_026-sprites-palette-panel +- No start_of_day files changed +- No DB/API/schema files changed +- No stale PR #219-#228 code copied + +## Requirement Checklist + +| Requirement | Status | Notes | +| --- | --- | --- | +| Add live preview | PASS | Preview canvas reflects unsaved draft pixels. | +| Add PNG export/download | PASS | Download button exports `sprite-creator-draft.png`. | +| No save-to-library | PASS | No saved sprite library or product persistence added. | +| No publishing | PASS | Export is local download only. | +| Avoid persisted data URLs | PASS | Uses `canvas.toBlob`; no `imageDataUrl` or `toDataURL`. | +| No DB/API/schema changes | PASS | Only UI/JS/CSS/test/report files changed. | + +## Validation Lane Report + +Commands: + +```text +node --check assets/toolbox/sprites/js/index.js +node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs +git diff --check -- toolbox/sprites/index.html assets/toolbox/sprites/js/index.js assets/theme-v2/css/gamefoundrystudio.css dev/tests/playwright/tools/SpritesToolShell.spec.mjs +rg --pcre2 -n -i "localStorage|sessionStorage|indexedDB|imageDataUrl|toDataURL|]+src=)|on(click|change|submit|input|load|error)=|local-mem|fake-login|MEM DB" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs +npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list --output= +``` + +Results: + +- Node syntax checks: PASS +- `git diff --check`: PASS +- Guard scan: PASS, no matches +- Targeted Playwright: PASS, 1 test passed + +## Manual Validation Notes + +1. Open `/toolbox/sprites/index.html` from the stacked branch. +2. Draw or fill pixels. +3. Confirm the preview canvas shows the draft. +4. Click Download PNG and confirm a `sprite-creator-draft.png` download starts. +5. Confirm no save-to-library or publishing controls are present. + +## ZIP Path + +`dev/workspace/zip/PR_26179_CHARLIE_027-sprites-preview-export_delta.zip` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index ccd4e2703..2a0f79d1d 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -2,6 +2,6 @@ assets/toolbox/sprites/js/index.js assets/theme-v2/css/gamefoundrystudio.css dev/tests/playwright/tools/SpritesToolShell.spec.mjs -docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md +docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 7726a32e0..2b8a05bd2 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,230 +1,188 @@ diff --git a/assets/theme-v2/css/gamefoundrystudio.css b/assets/theme-v2/css/gamefoundrystudio.css -index f7c6b39ea..22fc1de11 100644 +index 22fc1de11..82dff717c 100644 --- a/assets/theme-v2/css/gamefoundrystudio.css +++ b/assets/theme-v2/css/gamefoundrystudio.css -@@ -1199,6 +1199,46 @@ body.tool-focus-mode .tool-column:last-of-type { - background: var(--text) +@@ -1239,6 +1239,24 @@ body.tool-focus-mode .tool-column:last-of-type { + box-shadow: 0 1px 4px var(--swatch-shadow-color) } -+.sprite-canvas-cell--ink, -+.sprite-color-chip--ink { -+ background: var(--text) -+} -+ -+.sprite-canvas-cell--orange, -+.sprite-color-chip--orange { -+ background: var(--molten-orange) -+} -+ -+.sprite-canvas-cell--gold, -+.sprite-color-chip--gold { -+ background: var(--forge-gold) -+} -+ -+.sprite-canvas-cell--green, -+.sprite-color-chip--green { -+ background: var(--green) -+} -+ -+.sprite-canvas-cell--blue, -+.sprite-color-chip--blue { -+ background: var(--electric-blue) -+} -+ -+.sprite-palette-panel { ++.sprite-preview-shell { + display: flex; -+ flex-wrap: wrap; -+ gap: 8px ++ justify-content: center; ++ padding: 12px; ++ border: 1px solid var(--line); ++ border-radius: var(--radius-md); ++ background: var(--panel-soft) +} + -+.sprite-color-chip { -+ display: inline-block; -+ width: 14px; -+ height: 14px; -+ border: 1px solid var(--swatch-border); -+ border-radius: 50%; -+ box-shadow: 0 1px 4px var(--swatch-shadow-color) ++.sprite-preview-canvas { ++ width: min(100%, 160px); ++ height: auto; ++ aspect-ratio: 1; ++ border: 1px solid var(--line); ++ background: var(--card-background); ++ image-rendering: pixelated +} + @media(max-width:980px) { .grid.cols-4, diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js -index ae67b1b7c..f332496d1 100644 +index f332496d1..9db241a60 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js -@@ -1,11 +1,13 @@ - const DEFAULT_GRID_SIZE = 16; +@@ -2,6 +2,13 @@ const DEFAULT_GRID_SIZE = 16; const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]); const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]); -+const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]); + const EDITOR_COLOR_KEYS = Object.freeze(["ink", "orange", "gold", "green", "blue"]); ++const EDITOR_COLOR_CSS_VARIABLES = Object.freeze({ ++ blue: "--electric-blue", ++ gold: "--forge-gold", ++ green: "--green", ++ ink: "--text", ++ orange: "--molten-orange", ++}); const editorState = { activeTool: "pencil", -+ activeColor: "ink", - gridSize: DEFAULT_GRID_SIZE, -- paintedPixels: new Set(), -+ paintedPixels: new Map(), - }; +@@ -51,6 +58,7 @@ function updateDraftStatus() { + if (status) { + status.textContent = draftStatusText(); + } ++ renderPreview(); + } - function gridLabel(size) { -@@ -28,6 +30,22 @@ function draftStatusText() { - return `Unsaved editor state: ${count} draft pixel${count === 1 ? "" : "s"} painted.`; + function updatePaletteStatus() { +@@ -114,6 +122,64 @@ function fillGrid() { + updateDraftStatus(); } -+function normalizeColorKey(colorKey) { -+ return EDITOR_COLOR_KEYS.includes(colorKey) ? colorKey : "ink"; -+} -+ -+function clearCellColor(cell) { -+ cell.classList.remove(...EDITOR_COLOR_KEYS.map((colorKey) => `sprite-canvas-cell--${colorKey}`)); -+ delete cell.dataset.spriteColorKey; ++function editorColorValue(colorKey) { ++ const variableName = EDITOR_COLOR_CSS_VARIABLES[normalizeColorKey(colorKey)]; ++ const value = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim(); ++ return value || "#111111"; +} + -+function setCellColor(cell, colorKey) { -+ const normalizedColorKey = normalizeColorKey(colorKey); -+ clearCellColor(cell); -+ cell.classList.add("is-painted", `sprite-canvas-cell--${normalizedColorKey}`); -+ cell.dataset.spriteColorKey = normalizedColorKey; -+} -+ - function updateDraftStatus() { - const status = document.querySelector("[data-sprites-draft-status]"); - if (status) { -@@ -35,6 +53,24 @@ function updateDraftStatus() { - } - } - -+function updatePaletteStatus() { -+ const status = document.querySelector("[data-sprites-palette-status]"); -+ if (status) { -+ status.textContent = `Active editor color: ${editorState.activeColor[0].toUpperCase()}${editorState.activeColor.slice(1)}. Palette/Colors remains the reusable color source for future saved sprite records.`; ++function renderPreview() { ++ const canvas = document.querySelector("[data-sprites-preview-canvas]"); ++ if (!canvas) { ++ return; ++ } ++ const context = canvas.getContext("2d"); ++ if (!context) { ++ return; ++ } ++ const size = editorState.gridSize; ++ const cellSize = canvas.width / size; ++ context.clearRect(0, 0, canvas.width, canvas.height); ++ for (const [key, colorKey] of editorState.paintedPixels.entries()) { ++ const [rowText, columnText] = key.split(":"); ++ const row = Number(rowText); ++ const column = Number(columnText); ++ if (!Number.isFinite(row) || !Number.isFinite(column)) { ++ continue; ++ } ++ context.fillStyle = editorColorValue(colorKey); ++ context.fillRect((column - 1) * cellSize, (row - 1) * cellSize, cellSize, cellSize); + } +} + -+function setActiveColor(colorKey) { -+ const normalizedColorKey = normalizeColorKey(colorKey); -+ editorState.activeColor = normalizedColorKey; -+ document.querySelectorAll("[data-sprite-color-button]").forEach((button) => { -+ const isActive = button.dataset.spriteColorButton === normalizedColorKey; -+ button.classList.toggle("primary", isActive); -+ button.setAttribute("aria-pressed", String(isActive)); -+ }); -+ updatePaletteStatus(); ++function exportPreviewPng() { ++ const canvas = document.querySelector("[data-sprites-preview-canvas]"); ++ const status = document.querySelector("[data-sprites-export-status]"); ++ if (!canvas) { ++ return; ++ } ++ canvas.toBlob((blob) => { ++ if (!blob) { ++ if (status) { ++ status.textContent = "PNG export is unavailable in this browser session."; ++ } ++ return; ++ } ++ const objectUrl = URL.createObjectURL(blob); ++ const link = document.createElement("a"); ++ link.href = objectUrl; ++ link.download = "sprite-creator-draft.png"; ++ link.rel = "noopener"; ++ document.body.append(link); ++ link.click(); ++ link.remove(); ++ URL.revokeObjectURL(objectUrl); ++ if (status) { ++ status.textContent = "PNG downloaded from unsaved editor draft."; ++ } ++ }, "image/png"); +} + - function setActiveTool(toolName) { - if (!DRAWING_TOOLS.includes(toolName)) { - return; -@@ -56,9 +92,10 @@ function paintCell(cell) { - if (editorState.activeTool === "eraser") { - editorState.paintedPixels.delete(key); - cell.classList.remove("is-painted"); -+ clearCellColor(cell); - } else { -- editorState.paintedPixels.add(key); -- cell.classList.add("is-painted"); -+ editorState.paintedPixels.set(key, editorState.activeColor); -+ setCellColor(cell, editorState.activeColor); - } - updateDraftStatus(); - } -@@ -71,8 +108,8 @@ function fillGrid() { - editorState.paintedPixels.clear(); - grid.querySelectorAll("[data-sprite-pixel-row]").forEach((cell) => { - const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn); -- editorState.paintedPixels.add(key); -- cell.classList.add("is-painted"); -+ editorState.paintedPixels.set(key, editorState.activeColor); -+ setCellColor(cell, editorState.activeColor); - }); - updateDraftStatus(); - } -@@ -141,7 +178,17 @@ function wireDrawingTools() { + function setGridSize(size) { + const grid = document.querySelector("[data-sprites-pixel-grid]"); + const status = document.querySelector("[data-sprites-grid-status]"); +@@ -186,9 +252,18 @@ function wirePaletteButtons() { }); } -+function wirePaletteButtons() { -+ document.querySelectorAll("[data-sprite-color-button]").forEach((button) => { -+ button.addEventListener("click", () => { -+ setActiveColor(button.dataset.spriteColorButton); -+ }); -+ }); ++function wireExportButton() { ++ const button = document.querySelector("[data-sprites-export-png]"); ++ if (button) { ++ button.addEventListener("click", exportPreviewPng); ++ } +} + wireGridControls(); wireDrawingTools(); -+wirePaletteButtons(); + wirePaletteButtons(); ++wireExportButton(); setGridSize(DEFAULT_GRID_SIZE); setActiveTool(editorState.activeTool); -+setActiveColor(editorState.activeColor); + setActiveColor(editorState.activeColor); ++renderPreview(); diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -index 3fdc7decc..2e1825bf2 100644 +index 2e1825bf2..af9c022c7 100644 --- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -@@ -220,8 +220,8 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status - await expect(page.locator("[data-sprites-details-panel]")).toBeVisible(); - await expect(page.locator("[data-sprites-footer-status]")).toBeVisible(); - await expect(page.locator("[data-sprites-tools-panel]")).toContainText("Sprite Tools"); -- await expect(page.getByText("Drawing Tools")).toBeVisible(); -- await expect(page.getByText("Canvas Setup")).toBeVisible(); -+ await expect(page.getByText("Drawing Tools", { exact: true })).toBeVisible(); -+ await expect(page.getByText("Canvas Setup", { exact: true })).toBeVisible(); - await expect(page.getByRole("heading", { level: 2, name: "Pixel Work Area" })).toBeVisible(); - await expect(page.getByRole("heading", { level: 2, name: "Sprite Details" })).toBeVisible(); - await expect(page.locator("[data-sprites-toolbar]")).toBeVisible(); -@@ -232,6 +232,8 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status - await expect(page.getByRole("button", { name: `${toolName} tool placeholder` })).toBeDisabled(); - } - await expect(page.locator("[data-sprites-tool-status]")).toContainText("Pencil is active"); -+ await expect(page.locator("[data-sprites-palette-panel]")).toBeVisible(); -+ await expect(page.locator("[data-sprites-palette-status]")).toContainText("Active editor color: Ink"); - await expect(page.locator("[data-sprites-pixel-grid]")).toBeVisible(); - await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(256); - await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 16x16"); -@@ -248,11 +250,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status - await firstPixel.click(); - await expect(firstPixel).not.toHaveClass(/is-painted/); - await expect(page.locator("[data-sprites-draft-status]")).toContainText("empty draft"); -+ await page.getByRole("button", { name: "Gold editor color" }).click(); -+ await expect(page.locator("[data-sprites-palette-status]")).toContainText("Active editor color: Gold"); -+ await page.getByRole("button", { name: "Pencil tool" }).click(); -+ await firstPixel.click(); -+ await expect(firstPixel).toHaveClass(/sprite-canvas-cell--gold/); -+ await page.getByRole("button", { name: "Blue editor color" }).click(); - await page.getByRole("button", { name: "Fill tool" }).click(); +@@ -260,6 +260,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024); -+ await expect(page.locator("[data-sprites-pixel-grid] .sprite-canvas-cell--blue")).toHaveCount(1024); + await expect(page.locator("[data-sprites-pixel-grid] .sprite-canvas-cell--blue")).toHaveCount(1024); await expect(page.locator("[data-sprites-draft-status]")).toContainText("1024 draft pixels painted"); ++ await expect(page.locator("[data-sprites-preview-canvas]")).toBeVisible(); ++ const previewHasPaint = await page.locator("[data-sprites-preview-canvas]").evaluate((canvas) => { ++ const context = canvas.getContext("2d"); ++ const pixel = context.getImageData(1, 1, 1, 1).data; ++ return pixel[3] > 0; ++ }); ++ expect(previewHasPaint).toBe(true); ++ const downloadPromise = page.waitForEvent("download"); ++ await page.getByRole("button", { name: "Download PNG" }).click(); ++ const download = await downloadPromise; ++ expect(download.suggestedFilename()).toBe("sprite-creator-draft.png"); ++ await expect(page.locator("[data-sprites-export-status]")).toContainText("PNG downloaded"); await expect(page.locator("[data-sprites-shell-status]")).toContainText("Editor ready"); -- await expect(page.locator("main")).toContainText("Palette/Colors keys only"); -+ await expect(page.locator("main")).toContainText("Palette/Colors remains the reusable color source"); + await expect(page.locator("main")).toContainText("Palette/Colors remains the reusable color source"); await expect(page.locator("main")).not.toContainText(/Not implemented yet|future rebuild work|Static wireframe only|Plan sprite creation/i); - await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); - -diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md b/docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md +diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md b/docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md new file mode 100644 -index 000000000..8a4036c83 +index 000000000..f1a8fa62f --- /dev/null -+++ b/docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md -@@ -0,0 +1,61 @@ -+# PR_26179_CHARLIE_026-sprites-palette-panel ++++ b/docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md +@@ -0,0 +1,62 @@ ++# PR_26179_CHARLIE_027-sprites-preview-export + +Team: CHARLIE +Workflow: stacked feature workflow -+Base branch: PR_26179_CHARLIE_025-sprites-basic-drawing -+Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_026-sprites-palette-panel_delta.zip ++Base branch: PR_26179_CHARLIE_026-sprites-palette-panel ++Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_027-sprites-preview-export_delta.zip + +## Summary + -+Added an editor-only palette panel with active color selection. Selected editor colors apply to Pencil and Fill drawing in unsaved page-session state. The page still states that Palette/Colors remains the reusable color source for future saved sprite records. ++Added live preview and PNG download/export for the unsaved Sprite Creator editor draft. Export uses canvas `toBlob` and browser download behavior. This PR does not save to library, publish, write browser storage, or add API/DB/schema changes. + +## Branch Validation + +PASS + -+- Current branch: PR_26179_CHARLIE_026-sprites-palette-panel -+- Based on: PR_26179_CHARLIE_025-sprites-basic-drawing ++- Current branch: PR_26179_CHARLIE_027-sprites-preview-export ++- Based on: PR_26179_CHARLIE_026-sprites-palette-panel +- No start_of_day files changed +- No DB/API/schema files changed +- No stale PR #219-#228 code copied @@ -233,12 +191,12 @@ index 000000000..8a4036c83 + +| Requirement | Status | Notes | +| --- | --- | --- | -+| Add active color selection | PASS | Palette buttons update active editor color. | -+| Add basic preset palette | PASS | Ink, Orange, Gold, Green, and Blue editor presets are visible. | -+| Connect selected color to drawing tools | PASS | Pencil and Fill apply selected color classes. | -+| Creator-facing language | PASS | Copy describes editor draft colors and future Palette/Colors source. | -+| No DB/API | PASS | No API/database changes. | -+| No browser-owned authoritative product data | PASS | No product save or browser storage. | ++| Add live preview | PASS | Preview canvas reflects unsaved draft pixels. | ++| Add PNG export/download | PASS | Download button exports `sprite-creator-draft.png`. | ++| No save-to-library | PASS | No saved sprite library or product persistence added. | ++| No publishing | PASS | Export is local download only. | ++| Avoid persisted data URLs | PASS | Uses `canvas.toBlob`; no `imageDataUrl` or `toDataURL`. | ++| No DB/API/schema changes | PASS | Only UI/JS/CSS/test/report files changed. | + +## Validation Lane Report + @@ -248,7 +206,7 @@ index 000000000..8a4036c83 +node --check assets/toolbox/sprites/js/index.js +node --check dev/tests/playwright/tools/SpritesToolShell.spec.mjs +git diff --check -- toolbox/sprites/index.html assets/toolbox/sprites/js/index.js assets/theme-v2/css/gamefoundrystudio.css dev/tests/playwright/tools/SpritesToolShell.spec.mjs -+rg --pcre2 -n -i "localStorage|sessionStorage|indexedDB|]+src=)|on(click|change|submit|input|load|error)=|imageDataUrl|local-mem|fake-login|MEM DB" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs ++rg --pcre2 -n -i "localStorage|sessionStorage|indexedDB|imageDataUrl|toDataURL|]+src=)|on(click|change|submit|input|load|error)=|local-mem|fake-login|MEM DB" toolbox/sprites/index.html assets/toolbox/sprites/js/index.js dev/tests/playwright/tools/SpritesToolShell.spec.mjs +npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list --output= +``` + @@ -262,44 +220,45 @@ index 000000000..8a4036c83 +## Manual Validation Notes + +1. Open `/toolbox/sprites/index.html` from the stacked branch. -+2. Select Gold, use Pencil, and confirm the painted pixel uses Gold. -+3. Select Blue, use Fill, and confirm the grid uses Blue. -+4. Confirm the page states Palette/Colors remains the reusable color source for future saved records. ++2. Draw or fill pixels. ++3. Confirm the preview canvas shows the draft. ++4. Click Download PNG and confirm a `sprite-creator-draft.png` download starts. ++5. Confirm no save-to-library or publishing controls are present. + +## ZIP Path + -+`dev/workspace/zip/PR_26179_CHARLIE_026-sprites-palette-panel_delta.zip` ++`dev/workspace/zip/PR_26179_CHARLIE_027-sprites-preview-export_delta.zip` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index e1771c688..ccd4e2703 100644 +index ccd4e2703..2a0f79d1d 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -2,6 +2,6 @@ assets/toolbox/sprites/js/index.js assets/theme-v2/css/gamefoundrystudio.css dev/tests/playwright/tools/SpritesToolShell.spec.mjs --docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md -+docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md +-docs_build/dev/reports/PR_26179_CHARLIE_026-sprites-palette-panel.md ++docs_build/dev/reports/PR_26179_CHARLIE_027-sprites-preview-export.md docs_build/dev/reports/codex_changed_files.txt docs_build/dev/reports/codex_review.diff diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html -index e651fcb7c..1ec5872c0 100644 +index 1ec5872c0..330824b43 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html -@@ -74,8 +74,15 @@ -
- Palette Source -
--

Reusable colors come from Colors. Sprite Creator will reference Palette/Colors keys only.

--

Palette/Colors selection is reserved for the data-backed build slice.

-+

Reusable colors come from Colors. These editor presets are unsaved draft colors for testing drawing tools only.

-+
-+ -+ -+ -+ -+ -+
-+

Active editor color: Ink. Palette/Colors remains the reusable color source for future saved sprite records.

+@@ -169,6 +169,17 @@ +
- ++
++ Preview & Export ++
++

Preview the unsaved editor draft and download it as a PNG. This does not save to the sprite library or publish the asset.

++
++ ++
++ ++

PNG export is ready for the unsaved editor draft.

++
++
+
+ Readiness +
diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html index 1ec5872c0..330824b43 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html @@ -169,6 +169,17 @@

Sprite Details

+
+ Preview & Export +
+

Preview the unsaved editor draft and download it as a PNG. This does not save to the sprite library or publish the asset.

+
+ +
+ +

PNG export is ready for the unsaved editor draft.

+
+
Readiness