diff --git a/assets/theme-v2/css/gamefoundrystudio.css b/assets/theme-v2/css/gamefoundrystudio.css index 5886f125e..f7c6b39ea 100644 --- a/assets/theme-v2/css/gamefoundrystudio.css +++ b/assets/theme-v2/css/gamefoundrystudio.css @@ -1195,6 +1195,10 @@ body.tool-focus-mode .tool-column:last-of-type { opacity: 1 } +.sprite-canvas-cell.is-painted { + background: var(--text) +} + @media(max-width:980px) { .grid.cols-4, diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js index 22ef6dc6f..ae67b1b7c 100644 --- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js @@ -1,5 +1,12 @@ const DEFAULT_GRID_SIZE = 16; const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]); +const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]); + +const editorState = { + activeTool: "pencil", + gridSize: DEFAULT_GRID_SIZE, + paintedPixels: new Set(), +}; function gridLabel(size) { return `Sprite Creator ${size} by ${size} pixel canvas`; @@ -9,6 +16,67 @@ function buttonForSize(size) { return document.querySelector(`[data-sprites-grid-size="${size}"]`); } +function pixelKey(row, column) { + return `${row}:${column}`; +} + +function draftStatusText() { + const count = editorState.paintedPixels.size; + if (count === 0) { + return "Unsaved editor state: empty draft."; + } + return `Unsaved editor state: ${count} draft pixel${count === 1 ? "" : "s"} painted.`; +} + +function updateDraftStatus() { + const status = document.querySelector("[data-sprites-draft-status]"); + if (status) { + status.textContent = draftStatusText(); + } +} + +function setActiveTool(toolName) { + if (!DRAWING_TOOLS.includes(toolName)) { + return; + } + editorState.activeTool = toolName; + document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => { + const isActive = button.dataset.spriteToolButton === toolName; + button.classList.toggle("primary", isActive); + button.setAttribute("aria-pressed", String(isActive)); + }); + const status = document.querySelector("[data-sprites-tool-status]"); + if (status) { + status.textContent = `${toolName[0].toUpperCase()}${toolName.slice(1)} is active. Drawing stays in unsaved editor state for this page session only.`; + } +} + +function paintCell(cell) { + const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn); + if (editorState.activeTool === "eraser") { + editorState.paintedPixels.delete(key); + cell.classList.remove("is-painted"); + } else { + editorState.paintedPixels.add(key); + cell.classList.add("is-painted"); + } + updateDraftStatus(); +} + +function fillGrid() { + const grid = document.querySelector("[data-sprites-pixel-grid]"); + if (!grid) { + return; + } + 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"); + }); + updateDraftStatus(); +} + function setGridSize(size) { const grid = document.querySelector("[data-sprites-pixel-grid]"); const status = document.querySelector("[data-sprites-grid-status]"); @@ -17,6 +85,8 @@ function setGridSize(size) { } grid.replaceChildren(); + editorState.gridSize = size; + editorState.paintedPixels.clear(); grid.dataset.spritesGridSize = String(size); grid.setAttribute("aria-label", gridLabel(size)); @@ -26,11 +96,11 @@ function setGridSize(size) { const cell = document.createElement("button"); cell.className = "sprite-canvas-cell"; cell.type = "button"; - cell.disabled = true; cell.setAttribute("role", "gridcell"); cell.setAttribute("aria-label", `Pixel row ${row}, column ${column}`); cell.dataset.spritePixelRow = String(row); cell.dataset.spritePixelColumn = String(column); + cell.addEventListener("click", () => paintCell(cell)); grid.append(cell); } @@ -43,6 +113,7 @@ function setGridSize(size) { if (status) { status.textContent = `Canvas display mode: ${size}x${size}. No pixel data is saved.`; } + updateDraftStatus(); } function wireGridControls() { @@ -55,5 +126,22 @@ function wireGridControls() { }); } +function wireDrawingTools() { + document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => { + const toolName = button.dataset.spriteToolButton; + if (!DRAWING_TOOLS.includes(toolName) || button.disabled) { + return; + } + button.addEventListener("click", () => { + setActiveTool(toolName); + if (toolName === "fill") { + fillGrid(); + } + }); + }); +} + wireGridControls(); +wireDrawingTools(); setGridSize(DEFAULT_GRID_SIZE); +setActiveTool(editorState.activeTool); diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs index 3b7805669..3fdc7decc 100644 --- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs @@ -10,9 +10,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, "..", "..", "..", ".."); const SPRITE_TOOLBAR_PLACEHOLDERS = [ - "Pencil", - "Eraser", - "Fill", "Line", "Rectangle", "Circle", @@ -228,10 +225,13 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status 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(); + await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled(); + await expect(page.getByRole("button", { name: "Fill tool" })).toBeEnabled(); for (const toolName of SPRITE_TOOLBAR_PLACEHOLDERS) { await expect(page.getByRole("button", { name: `${toolName} tool placeholder` })).toBeDisabled(); } - await expect(page.locator("main")).toContainText("Toolbar placeholders only"); + await expect(page.locator("[data-sprites-tool-status]")).toContainText("Pencil is active"); 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"); @@ -240,7 +240,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(1024); await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 32x32"); await expect(page.getByRole("button", { name: "32x32" })).toHaveAttribute("aria-pressed", "true"); - await expect(page.locator("[data-sprites-shell-status]")).toContainText("Shell ready"); + const firstPixel = page.locator("[data-sprites-pixel-grid] [role='gridcell']").first(); + await firstPixel.click(); + await expect(firstPixel).toHaveClass(/is-painted/); + await expect(page.locator("[data-sprites-draft-status]")).toContainText("1 draft pixel painted"); + await page.getByRole("button", { name: "Eraser tool" }).click(); + 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: "Fill tool" }).click(); + await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024); + await expect(page.locator("[data-sprites-draft-status]")).toContainText("1024 draft pixels painted"); + 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")).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_025-sprites-basic-drawing.md b/docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md new file mode 100644 index 000000000..7f4879aa8 --- /dev/null +++ b/docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md @@ -0,0 +1,64 @@ +# PR_26179_CHARLIE_025-sprites-basic-drawing + +Team: CHARLIE +Workflow: stacked feature workflow +Base branch: PR_26179_CHARLIE_024-sprites-canvas-grid +Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_025-sprites-basic-drawing_delta.zip + +## Summary + +Implemented basic Sprite Creator drawing for Pencil, Eraser, and Fill on the visible pixel canvas. Drawing state is page-session editor state only and is clearly labeled as unsaved. No product save, browser storage, API, DB, schema, or publishing behavior was added. + +## Branch Validation + +PASS + +- Current branch: PR_26179_CHARLIE_025-sprites-basic-drawing +- Based on: PR_26179_CHARLIE_024-sprites-canvas-grid +- No start_of_day files changed +- No DB/API/schema files changed +- No stale PR #219-#228 code copied + +## Requirement Checklist + +| Requirement | Status | Notes | +| --- | --- | --- | +| Implement Pencil | PASS | Clicking a pixel paints it in unsaved editor state. | +| Implement Eraser | PASS | Clicking a painted pixel clears it. | +| Implement Fill | PASS | Fill paints the current grid. | +| Page-session editor state only | PASS | State lives in the module runtime only. | +| Clearly marked unsaved | PASS | Status copy labels unsaved editor state. | +| No product save | PASS | No save/load/publishing contract added. | +| No browser-owned authoritative product data | PASS | No browser storage or persistence added. | +| 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|]+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 +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. Click a pixel with Pencil active and confirm it paints. +3. Choose Eraser, click the painted pixel, and confirm it clears. +4. Choose Fill and confirm the visible grid fills. +5. Confirm copy says the state is unsaved editor state. + +## ZIP Path + +`dev/workspace/zip/PR_26179_CHARLIE_025-sprites-basic-drawing_delta.zip` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index 01ac40869..e1771c688 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_024-sprites-canvas-grid.md +docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.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 88ab1cceb..b58f130a4 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,162 +1,228 @@ diff --git a/assets/theme-v2/css/gamefoundrystudio.css b/assets/theme-v2/css/gamefoundrystudio.css -index 3f915bf6a..5886f125e 100644 +index 5886f125e..f7c6b39ea 100644 --- a/assets/theme-v2/css/gamefoundrystudio.css +++ b/assets/theme-v2/css/gamefoundrystudio.css -@@ -1153,6 +1153,48 @@ body.tool-focus-mode .tool-column:last-of-type { - grid-row: 1 / span 2 +@@ -1195,6 +1195,10 @@ body.tool-focus-mode .tool-column:last-of-type { + opacity: 1 } -+.sprite-canvas-shell { -+ display: flex; -+ justify-content: center; -+ padding: 16px; -+ border: 1px solid var(--line); -+ border-radius: var(--radius-md); -+ background: var(--panel-soft) -+} -+ -+.sprite-canvas-grid { -+ --sprite-grid-size: 16; -+ display: grid; -+ grid-template-columns: repeat(var(--sprite-grid-size), minmax(0, 1fr)); -+ width: min(100%, 520px); -+ aspect-ratio: 1; -+ border: 1px solid var(--line); -+ background: var(--panel) -+} -+ -+.sprite-canvas-grid[data-sprites-grid-size="16"] { -+ --sprite-grid-size: 16 -+} -+ -+.sprite-canvas-grid[data-sprites-grid-size="32"] { -+ --sprite-grid-size: 32 -+} -+ -+.sprite-canvas-cell { -+ min-width: 0; -+ min-height: 0; -+ padding: 0; -+ border: 0; -+ border-right: 1px solid var(--line); -+ border-bottom: 1px solid var(--line); -+ background: var(--card-background); -+ cursor: default -+} -+ -+.sprite-canvas-cell:disabled { -+ opacity: 1 ++.sprite-canvas-cell.is-painted { ++ background: var(--text) +} + @media(max-width:980px) { .grid.cols-4, diff --git a/assets/toolbox/sprites/js/index.js b/assets/toolbox/sprites/js/index.js -new file mode 100644 -index 000000000..22ef6dc6f ---- /dev/null +index 22ef6dc6f..ae67b1b7c 100644 +--- a/assets/toolbox/sprites/js/index.js +++ b/assets/toolbox/sprites/js/index.js -@@ -0,0 +1,59 @@ -+const DEFAULT_GRID_SIZE = 16; -+const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]); -+ -+function gridLabel(size) { -+ return `Sprite Creator ${size} by ${size} pixel canvas`; +@@ -1,5 +1,12 @@ + const DEFAULT_GRID_SIZE = 16; + const SUPPORTED_GRID_SIZES = Object.freeze([16, 32]); ++const DRAWING_TOOLS = Object.freeze(["pencil", "eraser", "fill"]); ++ ++const editorState = { ++ activeTool: "pencil", ++ gridSize: DEFAULT_GRID_SIZE, ++ paintedPixels: new Set(), ++}; + + function gridLabel(size) { + return `Sprite Creator ${size} by ${size} pixel canvas`; +@@ -9,6 +16,67 @@ function buttonForSize(size) { + return document.querySelector(`[data-sprites-grid-size="${size}"]`); + } + ++function pixelKey(row, column) { ++ return `${row}:${column}`; +} + -+function buttonForSize(size) { -+ return document.querySelector(`[data-sprites-grid-size="${size}"]`); ++function draftStatusText() { ++ const count = editorState.paintedPixels.size; ++ if (count === 0) { ++ return "Unsaved editor state: empty draft."; ++ } ++ return `Unsaved editor state: ${count} draft pixel${count === 1 ? "" : "s"} painted.`; +} + -+function setGridSize(size) { -+ const grid = document.querySelector("[data-sprites-pixel-grid]"); -+ const status = document.querySelector("[data-sprites-grid-status]"); -+ if (!grid || !SUPPORTED_GRID_SIZES.includes(size)) { -+ return; ++function updateDraftStatus() { ++ const status = document.querySelector("[data-sprites-draft-status]"); ++ if (status) { ++ status.textContent = draftStatusText(); + } ++} + -+ grid.replaceChildren(); -+ grid.dataset.spritesGridSize = String(size); -+ grid.setAttribute("aria-label", gridLabel(size)); -+ -+ for (let index = 0; index < size * size; index += 1) { -+ const row = Math.floor(index / size) + 1; -+ const column = (index % size) + 1; -+ const cell = document.createElement("button"); -+ cell.className = "sprite-canvas-cell"; -+ cell.type = "button"; -+ cell.disabled = true; -+ cell.setAttribute("role", "gridcell"); -+ cell.setAttribute("aria-label", `Pixel row ${row}, column ${column}`); -+ cell.dataset.spritePixelRow = String(row); -+ cell.dataset.spritePixelColumn = String(column); -+ grid.append(cell); ++function setActiveTool(toolName) { ++ if (!DRAWING_TOOLS.includes(toolName)) { ++ return; + } -+ -+ document.querySelectorAll("[data-sprites-grid-size]").forEach((button) => { -+ const isActive = button.dataset.spritesGridSize === String(size); ++ editorState.activeTool = toolName; ++ document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => { ++ const isActive = button.dataset.spriteToolButton === toolName; + button.classList.toggle("primary", isActive); + button.setAttribute("aria-pressed", String(isActive)); + }); -+ ++ const status = document.querySelector("[data-sprites-tool-status]"); + if (status) { -+ status.textContent = `Canvas display mode: ${size}x${size}. No pixel data is saved.`; ++ status.textContent = `${toolName[0].toUpperCase()}${toolName.slice(1)} is active. Drawing stays in unsaved editor state for this page session only.`; ++ } ++} ++ ++function paintCell(cell) { ++ const key = pixelKey(cell.dataset.spritePixelRow, cell.dataset.spritePixelColumn); ++ if (editorState.activeTool === "eraser") { ++ editorState.paintedPixels.delete(key); ++ cell.classList.remove("is-painted"); ++ } else { ++ editorState.paintedPixels.add(key); ++ cell.classList.add("is-painted"); + } ++ updateDraftStatus(); ++} ++ ++function fillGrid() { ++ const grid = document.querySelector("[data-sprites-pixel-grid]"); ++ if (!grid) { ++ return; ++ } ++ 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"); ++ }); ++ updateDraftStatus(); +} + -+function wireGridControls() { -+ SUPPORTED_GRID_SIZES.forEach((size) => { -+ const button = buttonForSize(size); -+ if (!button) { + function setGridSize(size) { + const grid = document.querySelector("[data-sprites-pixel-grid]"); + const status = document.querySelector("[data-sprites-grid-status]"); +@@ -17,6 +85,8 @@ function setGridSize(size) { + } + + grid.replaceChildren(); ++ editorState.gridSize = size; ++ editorState.paintedPixels.clear(); + grid.dataset.spritesGridSize = String(size); + grid.setAttribute("aria-label", gridLabel(size)); + +@@ -26,11 +96,11 @@ function setGridSize(size) { + const cell = document.createElement("button"); + cell.className = "sprite-canvas-cell"; + cell.type = "button"; +- cell.disabled = true; + cell.setAttribute("role", "gridcell"); + cell.setAttribute("aria-label", `Pixel row ${row}, column ${column}`); + cell.dataset.spritePixelRow = String(row); + cell.dataset.spritePixelColumn = String(column); ++ cell.addEventListener("click", () => paintCell(cell)); + grid.append(cell); + } + +@@ -43,6 +113,7 @@ function setGridSize(size) { + if (status) { + status.textContent = `Canvas display mode: ${size}x${size}. No pixel data is saved.`; + } ++ updateDraftStatus(); + } + + function wireGridControls() { +@@ -55,5 +126,22 @@ function wireGridControls() { + }); + } + ++function wireDrawingTools() { ++ document.querySelectorAll("[data-sprite-tool-button]").forEach((button) => { ++ const toolName = button.dataset.spriteToolButton; ++ if (!DRAWING_TOOLS.includes(toolName) || button.disabled) { + return; + } -+ button.addEventListener("click", () => setGridSize(size)); ++ button.addEventListener("click", () => { ++ setActiveTool(toolName); ++ if (toolName === "fill") { ++ fillGrid(); ++ } ++ }); + }); +} + -+wireGridControls(); -+setGridSize(DEFAULT_GRID_SIZE); + wireGridControls(); ++wireDrawingTools(); + setGridSize(DEFAULT_GRID_SIZE); ++setActiveTool(editorState.activeTool); diff --git a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -index b2c3d9922..3b7805669 100644 +index 3b7805669..3fdc7decc 100644 --- a/dev/tests/playwright/tools/SpritesToolShell.spec.mjs +++ b/dev/tests/playwright/tools/SpritesToolShell.spec.mjs -@@ -233,6 +233,13 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status +@@ -10,9 +10,6 @@ const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const repoRoot = path.resolve(__dirname, "..", "..", "..", ".."); + const SPRITE_TOOLBAR_PLACEHOLDERS = [ +- "Pencil", +- "Eraser", +- "Fill", + "Line", + "Rectangle", + "Circle", +@@ -228,10 +225,13 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status + 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(); ++ await expect(page.getByRole("button", { name: "Pencil tool" })).toBeEnabled(); ++ await expect(page.getByRole("button", { name: "Eraser tool" })).toBeEnabled(); ++ await expect(page.getByRole("button", { name: "Fill tool" })).toBeEnabled(); + for (const toolName of SPRITE_TOOLBAR_PLACEHOLDERS) { + await expect(page.getByRole("button", { name: `${toolName} tool placeholder` })).toBeDisabled(); } - await expect(page.locator("main")).toContainText("Toolbar placeholders only"); +- await expect(page.locator("main")).toContainText("Toolbar placeholders only"); ++ await expect(page.locator("[data-sprites-tool-status]")).toContainText("Pencil is active"); 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"); -+ await page.getByRole("button", { name: "32x32" }).click(); -+ await expect(page.locator("[data-sprites-pixel-grid]")).toHaveAttribute("aria-label", "Sprite Creator 32 by 32 pixel canvas"); -+ await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(1024); -+ await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 32x32"); -+ await expect(page.getByRole("button", { name: "32x32" })).toHaveAttribute("aria-pressed", "true"); - await expect(page.locator("[data-sprites-shell-status]")).toContainText("Shell ready"); + 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"); +@@ -240,7 +240,18 @@ test("Sprite Creator shell loads with visible tool, canvas, details, and status + await expect(page.locator("[data-sprites-pixel-grid] [role='gridcell']")).toHaveCount(1024); + await expect(page.locator("[data-sprites-grid-status]")).toContainText("Canvas display mode: 32x32"); + await expect(page.getByRole("button", { name: "32x32" })).toHaveAttribute("aria-pressed", "true"); +- await expect(page.locator("[data-sprites-shell-status]")).toContainText("Shell ready"); ++ const firstPixel = page.locator("[data-sprites-pixel-grid] [role='gridcell']").first(); ++ await firstPixel.click(); ++ await expect(firstPixel).toHaveClass(/is-painted/); ++ await expect(page.locator("[data-sprites-draft-status]")).toContainText("1 draft pixel painted"); ++ await page.getByRole("button", { name: "Eraser tool" }).click(); ++ 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: "Fill tool" }).click(); ++ await expect(page.locator("[data-sprites-pixel-grid] .is-painted")).toHaveCount(1024); ++ await expect(page.locator("[data-sprites-draft-status]")).toContainText("1024 draft pixels painted"); ++ 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")).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_024-sprites-canvas-grid.md b/docs_build/dev/reports/PR_26179_CHARLIE_024-sprites-canvas-grid.md + await expect(page.locator("style, [style], script:not([src])")).toHaveCount(0); +diff --git a/docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md b/docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md new file mode 100644 -index 000000000..4d7d89ce7 +index 000000000..7f4879aa8 --- /dev/null -+++ b/docs_build/dev/reports/PR_26179_CHARLIE_024-sprites-canvas-grid.md -@@ -0,0 +1,62 @@ -+# PR_26179_CHARLIE_024-sprites-canvas-grid ++++ b/docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.md +@@ -0,0 +1,64 @@ ++# PR_26179_CHARLIE_025-sprites-basic-drawing + +Team: CHARLIE +Workflow: stacked feature workflow -+Base branch: PR_26179_CHARLIE_023-sprites-toolbar-placeholders -+Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_024-sprites-canvas-grid_delta.zip ++Base branch: PR_26179_CHARLIE_024-sprites-canvas-grid ++Canonical ZIP path: dev/workspace/zip/PR_26179_CHARLIE_025-sprites-basic-drawing_delta.zip + +## Summary + -+Added a visible Sprite Creator pixel canvas grid with 16x16 and 32x32 display modes. The grid is rendered by external JavaScript and styled through Theme V2 CSS. This PR adds display behavior only; no persistence, drawing, API, DB, schema, or authoritative browser product data was added. ++Implemented basic Sprite Creator drawing for Pencil, Eraser, and Fill on the visible pixel canvas. Drawing state is page-session editor state only and is clearly labeled as unsaved. No product save, browser storage, API, DB, schema, or publishing behavior was added. + +## Branch Validation + +PASS + -+- Current branch: PR_26179_CHARLIE_024-sprites-canvas-grid -+- Based on: PR_26179_CHARLIE_023-sprites-toolbar-placeholders ++- Current branch: PR_26179_CHARLIE_025-sprites-basic-drawing ++- Based on: PR_26179_CHARLIE_024-sprites-canvas-grid +- No start_of_day files changed +- No DB/API/schema files changed +- No stale PR #219-#228 code copied @@ -165,13 +231,14 @@ index 000000000..4d7d89ce7 + +| Requirement | Status | Notes | +| --- | --- | --- | -+| Add visible pixel canvas grid | PASS | Grid renders on page load. | -+| Support 16x16 display mode | PASS | Default grid has 256 cells. | -+| Support 32x32 display mode | PASS | Toggle renders 1024 cells. | -+| No persistence | PASS | No storage/API/database writes. | -+| No drawing behavior | PASS | Pixel cells are disabled display cells. | -+| Theme V2 compliant | PASS | CSS added to shared Theme V2 stylesheet; no inline styles. | -+| No DB/API/schema changes | PASS | Only shell, CSS, JS, test, reports changed. | ++| Implement Pencil | PASS | Clicking a pixel paints it in unsaved editor state. | ++| Implement Eraser | PASS | Clicking a painted pixel clears it. | ++| Implement Fill | PASS | Fill paints the current grid. | ++| Page-session editor state only | PASS | State lives in the module runtime only. | ++| Clearly marked unsaved | PASS | Status copy labels unsaved editor state. | ++| No product save | PASS | No save/load/publishing contract added. | ++| No browser-owned authoritative product data | PASS | No browser storage or persistence added. | ++| No DB/API/schema changes | PASS | Only UI/JS/CSS/test/report files changed. | + +## Validation Lane Report + @@ -181,7 +248,7 @@ index 000000000..4d7d89ce7 +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 "]+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|]+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 +npx playwright test dev/tests/playwright/tools/SpritesToolShell.spec.mjs --workers=1 --reporter=list --output= +``` + @@ -195,63 +262,66 @@ index 000000000..4d7d89ce7 +## Manual Validation Notes + +1. Open `/toolbox/sprites/index.html` from the stacked branch. -+2. Confirm the Pixel Grid renders in 16x16 mode by default. -+3. Click 32x32 and confirm the grid updates and the status text changes. -+4. Confirm no pixel drawing or save/load behavior exists yet. ++2. Click a pixel with Pencil active and confirm it paints. ++3. Choose Eraser, click the painted pixel, and confirm it clears. ++4. Choose Fill and confirm the visible grid fills. ++5. Confirm copy says the state is unsaved editor state. + +## ZIP Path + -+`dev/workspace/zip/PR_26179_CHARLIE_024-sprites-canvas-grid_delta.zip` ++`dev/workspace/zip/PR_26179_CHARLIE_025-sprites-basic-drawing_delta.zip` diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -index 3947ffbc8..01ac40869 100644 +index 01ac40869..e1771c688 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,5 +1,7 @@ - toolbox/sprites/index.html -+assets/toolbox/sprites/js/index.js -+assets/theme-v2/css/gamefoundrystudio.css +@@ -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_023-sprites-toolbar-placeholders.md -+docs_build/dev/reports/PR_26179_CHARLIE_024-sprites-canvas-grid.md +-docs_build/dev/reports/PR_26179_CHARLIE_024-sprites-canvas-grid.md ++docs_build/dev/reports/PR_26179_CHARLIE_025-sprites-basic-drawing.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 cbedd6703..bf32a281b 100644 +index bf32a281b..e651fcb7c 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html -@@ -95,20 +95,14 @@ -
Canvas Preview
-

Pixel Grid

+@@ -35,9 +35,9 @@ +
+

Choose the creation action that will shape the sprite canvas in a later editor slice.

+
+- +- +- ++ ++ ++ + + + +@@ -45,7 +45,7 @@ + + +
+-

Toolbar placeholders only. Drawing behavior is not implemented in this PR.

++

Pencil is active. Drawing stays in unsaved editor state for this page session only.

--
-- -- -- -- -- -- -- -- -- -- -- --
1,11,21,31,41,51,61,71,8
2,12,22,32,42,52,62,72,8
3,13,23,33,43,53,63,73,8
4,14,24,34,44,54,64,74,8
5,15,25,35,45,55,65,75,8
6,16,26,36,46,56,66,76,8
7,17,27,37,47,57,67,77,8
8,18,28,38,48,58,68,78,8
-+
-+ -+ + +
+@@ -103,6 +103,7 @@ +
-+
-+
-+
-+

Canvas display mode: 16x16. No pixel data is saved.

+

Canvas display mode: 16x16. No pixel data is saved.

++

Unsaved editor state: empty draft.

-@@ -184,6 +178,7 @@ -
- - -+ - - - +@@ -111,7 +112,7 @@ +
Status
+

Sprite Creator Shell

+ +-

Shell ready. Drawing, Palette/Colors references, and save/load data contracts remain reserved for later scoped PRs.

++

Editor ready. Pencil, Eraser, and Fill affect unsaved page-session draft pixels only; save/load data contracts remain reserved for later scoped PRs.

+ +
+ diff --git a/toolbox/sprites/index.html b/toolbox/sprites/index.html index bf32a281b..e651fcb7c 100644 --- a/toolbox/sprites/index.html +++ b/toolbox/sprites/index.html @@ -35,9 +35,9 @@

Sprite Tools

Choose the creation action that will shape the sprite canvas in a later editor slice.

- - - + + + @@ -45,7 +45,7 @@

Sprite Tools

-

Toolbar placeholders only. Drawing behavior is not implemented in this PR.

+

Pencil is active. Drawing stays in unsaved editor state for this page session only.

@@ -103,6 +103,7 @@

Pixel Grid

Canvas display mode: 16x16. No pixel data is saved.

+

Unsaved editor state: empty draft.

@@ -111,7 +112,7 @@

Pixel Grid

Status

Sprite Creator Shell

-

Shell ready. Drawing, Palette/Colors references, and save/load data contracts remain reserved for later scoped PRs.

+

Editor ready. Pencil, Eraser, and Fill affect unsaved page-session draft pixels only; save/load data contracts remain reserved for later scoped PRs.